存储卡视频回放

更新时间:2023-09-19 03:00:53下载pdf

涂鸦 IP 摄像机开发支持存储卡录制功能。IP 摄像机插入存储卡后,可以查看存储卡的信息和状态,并设置录像开关和模式。

功能说明

IP 摄像机在存储卡中保存视频录像后,可以通过 IPC SDK 在 App 端播放视频录像。同实时视频直播一样,在开始回放前,您需要先连接上 P2P(Peer-to-peer)通道。P2P 通道连接成功后,用户可以查询到设备端存储卡中录制的视频片段时间信息,然后播放视频片段。

更多详情,请参考 存储卡管理

不要对同一个设备,同时创建两个 ThingSmartCameraType 对象,否则会导致资源错误释放出现异常情况。

回放流程

存储卡视频回放

视频片段

设备端保存在存储卡中的视频片段,最长为 10 分钟一段,最短为 10 秒钟一段。IPC SDK 支持以天为单位查看和播放视频录像,并且能够查询某年某月中,哪几天保存过视频录像,以便于用户查看。查询结果通过 ThingSmartCameraDelegate 中的代理方法返回。

查询保存过视频的月份

接口说明

查询某年某月中保存过视频录像的日期。

- (void)queryRecordDaysWithYear:(NSUInteger)year month:(NSUInteger)month;

参数说明

参数 说明
year 年,例如 2020
month 月,例如 2

代理回调

查询有视频录像的日期结果回调。

- (void)camera:(id<ThingSmartCameraType>)camera didReceiveRecordDayQueryData:(NSArray<NSNumber *> *)days;

参数说明

参数 说明
camera 执行查询操作的摄像机对象
days 有视频录像的日期数组,如 @[@(1), @(2)] 表示查询的当月,1、2 号有视频录像,失败返回空数组

查询某日的视频片段

接口说明

查询某年某月某日的所有视频录像片段。

- (void)queryRecordTimeSliceWithYear:(NSUInteger)year month:(NSUInteger)month day:(NSUInteger)day;

参数说明

参数 说明
year 年,例如 2020
month 月,例如 2
day 日,例如 22

代理回调

当查询某天中的所有视频录像片段结果回调。

- (void)camera:(id<ThingSmartCameraType>)camera didReceiveTimeSliceQueryData:(NSArray<NSDictionary *> *)timeSlices;

参数说明

参数 说明
camera 执行查询操作的摄像机对象
timeSlices 当天的视频片段时间信息数组,失败返回空数组

timeSlices 数据类型

timeSlices 中的元素类型是 NSDictionary

字段(SDK 中的常量名) 类型 说明
kThingSmartPlaybackPeriodStartDate NSDate 视频片段开始时间
kThingSmartPlaybackPeriodStopDate NSDate 视频片段结束时间
kThingSmartPlaybackPeriodStartTime NSNumber 视频片段开始时间的 Unix 时间戳
kThingSmartPlaybackPeriodStopTime NSNumber 视频片段结束时间的 Unix 时间戳

视频播放

在成功查询到某天中的视频录像片段后,就可以开始播放录像视频了。

以下情况中,需要重新查询一下当天的视频录像片段,否则可能会出现播放异常:

  • 停止播放视频录像后,再进行实时视频播放。
  • P2P 连接断开,重新连接上 P2P 通道后,再次播放视频录像。

开始播放视频

在实时视频播放时,如果想要切换到录像播放模式,不需要断开 P2P 连接再重新连接 P2P 通道。但是需要先停止实时视频播放,再去查询当天的视频录像开始播放。否则,会出现实时视频画面和视频录像画面串流闪烁的情况。视频录像回放切换实时视频播放的时候同理。

接口说明

开始播放某段视频录像,playTime 的取值范围是 [startTime, stopTime)

- (void)startPlayback:(NSInteger)playTime startTime:(NSInteger)startTime stopTime:(NSInteger)stopTime;

参数说明

参数 说明
playTime 此段视频中,开始播放的时间点,使用 Unix 时间戳
startTime 此段视频的开始时间,使用 Unix 时间戳
stopTime 此段视频的结束时间,使用 Unix 时间戳

代理接口

- (void)cameraDidBeginPlayback:(id<ThingSmartCameraType>)camera;

暂停播放

接口说明

- (void)pausePlayback;

代理接口

视频录像已暂停播放。

- (void)cameraDidPausePlayback:(id<ThingSmartCameraType>)camera;

恢复播放

接口说明

- (void)resumePlayback;

代理接口

视频录像已恢复播放。

- (void)cameraDidResumePlayback:(id<ThingSmartCameraType>)camera;

停止播放

接口说明

- (void)stopPlayback;

代理接口

视频录像已停止播放

- (void)cameraDidStopPlayback:(id<ThingSmartCameraType>)camera;

播放结束回调

视频录像已结束播放。status1 时表示片段播放结束,其他值表示全天播放结束

接口说明

- (void)camera:(id<ThingSmartCameraType>)camera playbackTimeSlice:(NSDictionary *)timeSlice didFinishedWithStatus:(NSInteger)status;

连续播放

涂鸦 IP 摄像机的视频录像分两种录像模式,连续录像和事件录像。

  • 连续录像时,视频录像会是 10 分钟一个片段,且所有视频片段是连续的,但是如果中间有停止过视频录像,那么连续录像模式下的视频片段间也可能会有间隔。

    如果某天的视频录像片段是连续的,那么播放录像时,会自动播放下一段。也就是说,即使调用开始播放接口,传入的是当天第一个视频片段的时间点,视频也会一直播放到当天最后一个视频片段的最后一帧视频,才会回调视频播放结束的代理方法。

  • 事件录像时,每个录像片段长度不等,且片段间的间隔时间也长短不一。

    如果某天的视频录像片段是不连续的,例如视频录像片段 A 结束后,隔了一段时间才有视频录像片段 B。在播放到断开的地方(即播放到视频片段 A 的最后一帧),视频流会自动停止,IPC SDK 也不会收到视频录像播放结束的回调。这在最新涂鸦 IP 摄像机嵌入式 SDK 中已经修改为,视频片段不连续时,每一个片段播放结束都会回调视频录像播放结束,故开发者可以在收到视频播放结束的代理方法后,播放下一段视频录像,以达到连续播放的目的。但如果设备的固件版本不是最新的,则需要开发者通过 视频帧数据回调 方法,帧信息中的时间戳来判断当前帧是否是当前视频录像片段的最后一帧,手动判断当前视频录像片段是否播放结束。

暂停与停止

pausePlaybackstopPlayback都能达到停止播放视频的目的,它们之间的区别是:

  • 调用 stopPlayback 后,无法调用resumePlayback来恢复播放。
  • 停止播放后,如果想要继续之前的进度开始播放,那么必须保存停止播放时,最后一帧视频的时间戳,以及播放的视频录像的时间片段,以重新调用startPlayback方法来继续播放。
  • 在成功查询到视频录像片段时间数据后,无论是正在播放录像视频,还是暂停播放,都可以直接调用 startPlayback 重新播放另一个视频录像片段,而不必先调用 stopPlayback 停止播放。

倍速播放

部分 IPC 设备在播放存储卡回放视频时,支持变速播放。在 P2P 连接成功后,可以通过 IPC SDK 查询设备支持的播放速度列表,并在回放视频播放过程中,更改视频播放的速度。

接口说明

查询 IPC 设备支持的播放速度列表,返回 ThingSmartCameraPlayBackSpeed 枚举类型的数组,需要在 P2P 连接成功之后调用。

- (NSArray<NSNumber *> *)getSupportPlaySpeedList;

ThingSmartCameraPlayBackSpeed 枚举说明

枚举值 说明
ThingSmartCameraPlayBackSpeed_05TIMES 0.5 倍速播放
ThingSmartCameraPlayBackSpeed_10TIMES 1 倍速播放
ThingSmartCameraPlayBackSpeed_20TIMES 2 倍速播放
ThingSmartCameraPlayBackSpeed_40TIMES 4 倍速播放
ThingSmartCameraPlayBackSpeed_80TIMES 8 倍速播放
ThingSmartCameraPlayBackSpeed_160TIMES 16 倍速播放
ThingSmartCameraPlayBackSpeed_320TIMES 32 倍速播放

接口说明

设置存储卡回放视频的播放速度。

- (void)speedPlayWithPlayBackSpeed:(ThingSmartCameraPlayBackSpeed)playBackSpeed;

参数说明

参数 类型 说明
playBackSpeed ThingSmartCameraPlayBackSpeed 视频播放速度,需要设备支持此速度

下载视频

部分 IPC 设备支持将存储卡回放视频下载到 App 上。

查询设备是否支持下载

该接口可以查询设备是否支持存储卡视频回放下载,但需要在 P2P 连接成功之后调用。

- (BOOL)isSupportPlaybackDownload;

开始下载视频

接口说明

- (int)downloadPlayBackVideoWithRange:(NSRange)timeRange filePath:(NSString *)videoPath success:(void(^)(NSString *filePath))success progress:(void(^)(NSUInteger progress))progress failure:(void(^)(NSError *error))failure;

参数说明

参数 类型 说明
timeRange NSRange 需要下载的视频的范围
videoPath NSString 视频保存的文件地址,需要有 .mp4 后缀
success Block 视频下载成功回调函数
progress Block 视频下载进度回调函数,参数为 0~100 之间的整数
failure Block 视频下载失败回调函数,返回负数表示下载失败

由于 IPC 设备对回放视频文件的检索能力限制,视频下载的范围,起始点都必须分别在某个视频片段内。即,期望下载的视频的开始时间(timeRange.loc)、结束时间(timeRange.loc+timeRange.len)需要在某两个视频片段的时间范围内。

暂停下载视频

接口说明

- (int)pausePlayBackDownloadWithResponse:(void (^)(int))callback;

参数说明

参数 类型 说明
callback Block 设备响应回调,参数值为 0 表示暂停成功,否则暂停失败

恢复下载视频

接口说明

- (int)resumePlayBackDownloadWithResponse:(void (^)(int))callback;

参数说明

参数 类型 说明
callback Block 设备响应回调,参数值为 0 表示恢复成功,否则恢复失败

停止下载视频

接口说明

- (int)stopPlayBackDownloadWithResponse:(void (^)(int errCode))callback;

参数说明

参数 类型 说明
callback Block 设备响应回调,参数值为 0 表示停止成功,否则停止失败

删除视频

部分 IPC 设备支持通过 App 控制删除存储卡上某天的回放视频,目前只支持按日期删除全天的视频。

查询设备是否支持删除

该接口可以查询设备是否支持删除存储卡上的回放视频,但需要在 P2P 连接成功之后调用。

- (BOOL)isSupportPlaybackDelete;

删除指定日期的视频

接口说明

删除设备存储卡上指定日期的回放视频。

- (int)deletePlayBackDataWithDay:(NSString *)day onResponse:(void (^)(int errCode))callback onFinish:(void (^)(int errCode))finishedCallBack;

参数说明

参数 类型 说明
day NSString 期望删除的日期,格式为 yyyy-MM-dd
callback Block 命令下发结果回调,errCode 为负数,表示删除失败,否则表示已成功下发
finishedCallBack Block 设备删除视频文件结果回调,errCode 为负数,表示删除失败,否则表示成功删除当天所有视频文件

示例代码

Objective C

#define kThingSmartIPCConfigAPI @"thing.m.ipc.config.get"
#define kThingSmartIPCConfigAPIVersion @"2.0"

- (void)startPlayback {
    if (self.connected) {
        [self.camera queryRecordTimeSliceWithYear:2020 month:2 day:12];
        return;
    }
    id p2pType = [self.deviceModel.skills objectForKey:@"p2pType"];
    [[ThingSmartRequest new] requestWithApiName:kThingSmartIPCConfigAPI postData:@{@"devId": self.devId} version:kThingSmartIPCConfigAPIVersion success:^(id result) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            ThingSmartCameraConfig *config = [ThingSmartCameraFactory ipcConfigWithUid:[ThingSmartUser sharedInstance].uid localKey:self.deviceModel.localKey configData:result];
            self.camera = [ThingSmartCameraFactory cameraWithP2PType:p2pType config:config delegate:self];
            [self.camera connect];
        });
    } failure:^(NSError *error) {
        // 查询配置信息失败
    }];
}

- (void)pausePlayback {
    [self.camera pausePlayback];
}

- (void)resumePlayback {
    [self.camera resumePlayback];
}

- (void)stopPlayback {
    [self.camera stopPlayback];
}

#pragma mark - ThingSmartCameraDelegate

- (void)cameraDidConnected:(id<ThingSmartCameraType>)camera {
    self.connected = YES;
      // 需要 P2P 连接成功后查询某天的视频录像片段
        [camera queryRecordTimeSliceWithYear:2020 month:2 day:12];
}

- (void)cameraDisconnected:(id<ThingSmartCameraType>)camera specificErrorCode:(NSInteger)errorCode {
    // P2P 连接被动断开,一般为网络波动导致
    self.connected = NO;
    self.previewing = NO;
}


- (void)camera:(id<ThingSmartCameraType>)camera didReceiveTimeSliceQueryData:(NSArray<NSDictionary *> *)timeSlices {
      // 如果当天没有视频录像,则不播放
        if (timeSlices.count == 0) {
        return;
    }
      // 保存视频录像列表,从第一个开始播放
    self.timeSlicesInCurrentDay = [timeSlices copy];
      self.timeSlicesIndex = 0;
    NSDictionary *timeSlice = timeSlices.firstObject;
    NSInteger startTime = [timeSlice[kThingSmartTimeSliceStartTime] integerValue];
    NSInteger stopTime = [timeSlice[kThingSmartTimeSliceStopTime] integerValue];
      // 从第一个视频片段的第一秒开始播放
    NSInteger playTime = startTime;
    [camera startPlayback:playTime startTime:startTime stopTime:stopTime];
}

- (void)camera:(id<ThingSmartCameraType>)camera ty_didReceiveVideoFrame:(CMSampleBufferRef)sampleBuffer frameInfo:(ThingSmartVideoFrameInfo)frameInfo {
    NSInteger index = self.timeSlicesIndex + 1;
      // 如果没有下一个视频录像,则返回
    if (index >= self.timeSlicesInCurrentDay.count) {
        return;
    }
    NSDictionary *currentTimeSlice = [self.timeSlicesInCurrentDay objectAtIndex:self.timeSlicesIndex];
    NSInteger stopTime = [currentTimeSlice[kThingSmartTimeSliceStopTime] integerValue];
      // 如果当前视频帧的时间戳大于等于当前视频片段的结束时间,则播放下一个视频片段
    if (frameInfo.nTimeStamp >= stopTime) {
        NSDictionary *nextTimeSlice = [self.timeSlicesInCurrentDay objectAtIndex:index];
        NSInteger startTime = [nextTimeSlice[kThingSmartTimeSliceStartTime] integerValue];
            NSInteger stopTime = [nextTimeSlice[kThingSmartTimeSliceStopTime] integerValue];
            NSInteger playTime = startTime;
            [camera startPlayback:playTime startTime:startTime stopTime:stopTime];
    }
}

- (void)cameraDidBeginPlayback:(id<ThingSmartCameraType>)camera {
      // 视频录像开始播放
      self.playbacking = YES;
    self.playbackPaused = NO;
    // 将视频渲染视图添加到屏幕上
        [self.view addSubview:camera.videoView];
}

- (void)cameraDidPausePlayback:(id<ThingSmartCameraType>)camera {
      // 视频录像播放已暂停
    self.playbackPaused = YES;
}

- (void)cameraDidResumePlayback:(id<ThingSmartCameraType>)camera {
       // 视频录像已恢复播放
    self.playbackPaused = NO;
}

- (void)cameraDidStopPlayback:(id<ThingSmartCameraType>)camera {
      // 视频录像已停止播放
       self.playbacking = NO;
    self.playbackPaused = NO;
}

- (void)cameraPlaybackDidFinished:(id<ThingSmartCameraType>)camera {
      // 视频录像已结束播放
    self.playbacking = NO;
    self.playbackPaused = NO;
}

// 错误回调
- (void)camera:(id<ThingSmartCameraType>)camera didOccurredErrorAtStep:(TYCameraErrorCode)errStepCode specificErrorCode:(NSInteger)errorCode {
        if (errStepCode == TY_ERROR_CONNECT_FAILED) {
          // P2P 连接失败
        self.connected = NO;
    }
    else if (errStepCode == TY_ERROR_START_PLAYBACK_FAILED) {
          // 存储卡录像播放失败
        self.playbacking = NO;
            self.playbackPaused = NO;
    }
      else if (errStepCode == TY_ERROR_PAUSE_PLAYBACK_FAILED) {
                // 暂停播放失败
    }
    else if (errStepCode == TY_ERROR_RESUME_PLAYBACK_FAILED) {
                // 恢复播放失败
    }
}

Swift

func startPlayback() {
    if self.isConnected {
        self.camera.queryRecordTimeSlice(withYear: 2020, month: 2, day: 12)
        return
    }
    let p2pType = self.deviceModel.skills["p2pType"]!
    ThingSmartRequest().request(withApiName: kThingSmartIPCConfigAPI, postData: ["devId": self.devId], version: kThingSmartIPCConfigAPIVersion, success: { result in
        guard let responder = result as? [AnyHashable:Any] else {
            return;
        }
        DispatchQueue.global().async {
            let config = ThingSmartCameraFactory.ipcConfig(withUid: ThingSmartUser.sharedInstance().uid, localKey: self.deviceModel.localKey, configData: responder)
            self.camera = ThingSmartCameraFactory.camera(withP2PType: p2pType, config: config, delegate: self)
            self.camera.connect()
        }
    }) { _ in
        // 查询配置信息失败
    }
}

func pausePlayback() {
    self.camera.pausePlayback()
}

func resumePlayback() {
    self.camera.resumePlayback()
}

func stopPlayback() {
    self.camera.stopPlayback()
}

func cameraDidConnected(_ camera: ThingSmartCameraType!) {
    self.isConnected = true
    // 需要 P2P 连接成功后查询某天的视频录像片段
    camera.queryRecordTimeSlice(withYear: 2020, month: 2, day: 12)
}

func cameraDisconnected(_ camera: ThingSmartCameraType!, specificErrorCode: Int) {
    // P2P 连接被动断开,一般为网络波动导致
    self.isConnected = false
    self.isPreviewing = false
}

func camera(_ camera: ThingSmartCameraType!, didReceiveTimeSliceQueryData timeSlices: [[AnyHashable : Any]]!) {
    // 如果当天没有视频录像,则不播放
    guard timeSlices.count > 0 else {
        return;
    }
    // 保存视频录像列表,从第一个开始播放
    self.timeSlices = timeSlices
    self.timesliceIndex = 0
    let video = timeSlices.first!
    let startTime = video[kThingSmartTimeSliceStartTime] as! Int
    let stopTime = video[kThingSmartTimeSliceStopTime] as! Int
    // 从第一个视频片段的第一秒开始播放
    let playTime = startTime
    camera.startPlayback(playTime, startTime: startTime, stopTime: stopTime)
}

func camera(_ camera: ThingSmartCameraType!, ty_didReceiveVideoFrame sampleBuffer: CMSampleBuffer!, frameInfo: ThingSmartVideoFrameInfo) {
    let index = self.timesliceIndex + 1
    // 如果没有下一个视频录像,则返回
    guard index < self.timeSlices.count else {
        return
    }
    let currentTimeSlice = timeSlices[self.timesliceIndex]
    let endTime = currentTimeSlice[kThingSmartTimeSliceStopTime] as! Int
    guard frameInfo.nTimeStamp >= endTime else {
        return
    }
    // 如果当前视频帧的时间戳大于等于当前视频片段的结束时间,则播放下一个视频片段

    let nextTimeSlice = timeSlices.first!
    let startTime = nextTimeSlice[kThingSmartTimeSliceStartTime] as! Int
    let stopTime = nextTimeSlice[kThingSmartTimeSliceStopTime] as! Int
    let playTime = startTime
    camera.startPlayback(playTime, startTime: startTime, stopTime: stopTime)
}

func cameraDidBeginPlayback(_ camera: ThingSmartCameraType!) {
    // 视频录像开始播放
    self.isPlaybacking = true
    self.isPlaybackPaused = false
    // 将视频渲染视图添加到屏幕上
    self.view.addSubview(camera.videoView())
}

func cameraDidPausePlayback(_ camera: ThingSmartCameraType!) {
    // 视频录像播放已暂停
    self.isPlaybackPaused = true
}

func cameraDidResumePlayback(_ camera: ThingSmartCameraType!) {
    // 视频录像已恢复播放
    self.isPlaybackPaused = false
}

func cameraDidStopPlayback(_ camera: ThingSmartCameraType!) {
    // 视频录像已停止播放
    self.isPlaybacking = false
    self.isPlaybackPaused = false
}

func cameraPlaybackDidFinished(_ camera: ThingSmartCameraType!) {
    // 视频录像已结束播放
    self.isPlaybacking = false
    self.isPlaybackPaused = false
}

func camera(_ camera: ThingSmartCameraType!, didOccurredErrorAtStep errStepCode: TYCameraErrorCode, specificErrorCode errorCode: Int) {
    if errStepCode == TY_ERROR_CONNECT_FAILED  {
        // P2P 连接失败
        self.isConnected = false
    }else if errStepCode == TY_ERROR_START_PLAYBACK_FAILED {
        // 存储卡录像播放失败
        self.isPlaybacking = false
        self.isPlaybackPaused = false
    }else if errStepCode == TY_ERROR_PAUSE_PLAYBACK_FAILED {
        // 暂停播放失败
    }else if errStepCode == TY_ERROR_RESUME_PLAYBACK_FAILED {
        // 恢复播放失败
    }
}