产品文档 点播技术文档 iOS 点播回放 Core SDK 2.0

iOS 点播回放 Core SDK 2.0

版本说明

主要功能

SDK 功能主要分为点播回放下载三个部分

模块 功能 对应文件
点播 在线视频播放 BJVPlayerManager.h
本地视频播放(需要先下载)
播放控制:播放、暂停、跳转、切换清晰度等 BJVPlayProtocol.h
播放信息反馈:播放状态、视频时长、当前时间等
播放设置:倍速、后台播放、记忆播放、偏好清晰度等
回放 回放房间管理:创建(本地/在线)、进入、退出等 BJVRoom.h
课件显示
房间信息 BJVRoomVM.h
聊天消息:覆盖更新、增量更新 BJVMessageVM.h
用户信息 BJVOnlineUserVM.h
视频管理:与点播一致 BJVPlayProtocol.h
下载 点播、回放下载任务管理 BJVDownloadManager.h
1.x 版本 SDK 的点播、回放下载数据迁移 BJVDownloadManager+migration.h

Demo

点播、回放 的 UI SDK 是开源的,BJVideoPlayerCore 的集成可参考 BJVideoPlayerUI SDK demo,使用 开源的方式 集成。

引入 SDK

SDK 支持 iOS 9.0 及以上 的系统,iPhone、iPad 等设备,集成 2.0 或以上版本的 SDK 要求 Xcode 的版本至少为 9.0,由于 SDK 依赖关系复杂、手动配置繁琐,建议使用 CocoaPods 方式引入。存在 Cocoapods 导入失败的 spec 仓库时,通常可以查找镜像源,替换本地的 Cocoapods 中对应 spec 仓库的 source 的指向来解决。

Podfile 配置

  • Podfile 中设置 source
source 'https://github.com/CocoaPods/Specs.git'
source 'http://git.baijiashilian.com/open-ios/specs.git'

  • 指定版本,参考本文档的 '版本升级部分'
target 'yourProjectName' do
    pod 'BJVideoPlayerCore', '~>2.2.0'

# 用于动态引入 Framework,避免冲突问题
script_phase \
:name => '[BJLiveCore] Embed Frameworks',
:script => 'Pods/BJLiveCore/frameworks/EmbedFrameworks.sh',
:execution_position => :after_compile

script_phase \
:name => '[BJVideoPlayerCore] Embed Frameworks',
:script => 'Pods/BJVideoPlayerCore/frameworks/EmbedFrameworks.sh', 
:execution_position => :after_compile

# 用于清理动态引入的 Framework 用不到的架构,避免发布 AppStore 时发生错误,需要写在动态引入 Framework 的 script 之后
script_phase \
:name => '[BJLiveBase] Clear Archs From Frameworks',
:script => 'Pods/BJLiveBase/script/ClearArchsFromFrameworks.sh "BJHLMediaPlayer.framework" "BJYIJKMediaFramework.framework"',
:execution_position => :after_compile
end

全局设置

1. 配置ATS

需要在 info.plist 里面增加 NSAllowsArbitraryLoads = true

2. (可选)开启后台音频

如果有后台播放的需求,则必须开启此项:在 Project > Target > Capabilities 中打开 Background Modes 开关,选中 Audio, Airplay, and Picture in Picture

picture

3. 设置专属域名前缀

例如专属域名为 demo123.at.baijiayun.com,则前缀为 demo123,参考专属域名说明

// 引入头文件
#import <BJVideoPlayerCore/BJVAppConfig.h>
// 设置专属域名前缀
[[BJVAppConfig sharedInstance] setPrivateDomainPrefix:loginUser.privateDomainPrefix];

4. (可选)设置 BJVRequestTokenDelegate,管理 token

SDK 的点播、回放及下载功能都需要集成方传入 token 来向服务器请求数据,数据对应的 token 由集成方后台调用百家云 API 获取:点播 token回放 token,然后通知给 APP,APP 使用 token 等参数调用 SDK 的相关 API 实现功能。

集成方可配置 token 的有效期,当 SDK 发现点播、回放、下载的链接过期或不可用时,会通过 BJVRequestTokenDelegate 的方法回调,要求给对应的数据传入一个可用的 token。此时如果集成方在 delegate 的对应方法里重新获取一个可用的 token 返回给 SDK,SDK 就能继续正常工作,这样就实现了自动更新 token 的效果。如果集成方没有 token 过期和自动更新的需求,可以不设置delegate,SDK 会在发现链接过期或不可用时尝试使用旧的 token 重新请求服务器,请求失败将抛出 BJVErrorCode_invalidToken 错误,APP 执行错误处理的业务逻辑。

可以参考 BJVideoPlayerUI Demo 的 PUAppDelegate.m 文件中 BJVRequestTokenDelegate 方法的实现。BJVideoPlayerUI Demo 开发文档说明

// 用于在 点播播放/下载 中,集成方需要调用 completion 提供 videoID 对应的 token
- (void)requestTokenWithVideoID:(NSString *)videoID
                     completion:(void (^)(NSString * _Nullable token, NSError * _Nullable error))completion {
    // 示例:集成方获取点播 token 的方法
    [self getTokenWithVideoID:videoID
                   completion:^(NSString *token, NSError *error) {
                       completion(token, error);
                   }];
}

// 用于在 回放播放/下载 中,客户需要调用 completion 提供 classID、sessionID 对应的 token                     
- (void)requestTokenWithClassID:(NSString *)classID
                      sessionID:(nullable NSString *)sessionID
                     completion:(void (^)(NSString * _Nullable token, NSError * _Nullable error))completion {
    // 示例:集成方获取回放 token 的方法
    [self getTokenWithClassID:classID
                    sessionID:sessionID
                   completion:^(NSString *token, NSError *error) {
                       completion(token, error);
                   }];
}

要点说明

1. 属性、方法监听方式:Block

demo 及示例代码中大量使用特定的 block 进行属性、方法监听,可参考 直播文档中的对监听 block 的相关介绍。

点播功能集成

建议: 自主集成点播功能之前,可参考 BJVideoPlayerUI SDK 已经开源的代码,该 UI SDK 提供了清晰的点播模块 API 调用方式。

1. 引入头文件

#import <BJVideoPlayerCore/BJVideoPlayerCore.h>

2. 设置专属域名

参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

3. (可选)设置 BJVRequestTokenDelegate,管理 token

参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

4. 初始化播放器管理类 BJVPlayerManager

4.1 定义点播 manager 属性

@property (nonatomic) BJVPlayerManager *playerManager;

4.2 指定播放器类型,创建点播 manager 实例

点播播放器分为 AVPlayerIJKPlayer 两种,参考 BJVPlayerType

AVPlayer 是 iOS 系统的播放器,支持 m3u8 格式的视频,加载比较快,IJKPlayer 不支持。

IJKPlayer 是第三方开源播放器,基于 FFmpeg,支持加密和 flv 格式的视频,AVPlayer 不支持。

/**
 初始化 manager 实例

 #param playerType 播放器类型:AVPlayer、IJKPlayer
 #discussion AVPlayer 不支持加密
 #return manager 实例
 */
- (instancetype)initWithPlayerType:(BJVPlayerType)playerType;
// example:创建使用 AVPlayer 的 playerManager
self.manager = [[BJVPlayerManager alloc] initWithPlayerType:BJVPlayerType_AVPlayer];

5. 播放设置

播放器相关设置在 BJVPlayProtocol.h 文件中定义。

/** 进入后台时,是否继续播放音频,必须在工程的 background modes 中添加 audio 才会生效 */
self.manager.backgroundAudioEnabled = backgroundAudioEnabled;

/**
 偏好清晰度列表

 #discussion 优先使用此列表中的清晰度播放在线视频,优先级按数组元素顺序递减
 #discussion preferredDefinitionList 列表元素为清晰度的标识字符串,现有标识符:low(标清),high(高清),superHD(超清),720p,1080p,audio(纯音频),可根据实际情况动态扩展
 #discussion 此设置对播放本地视频无效
 */
self.manager.preferredDefinitionList = @[@"high","low"]; // 优先高清,其次标清

/** 是否开启记忆播放 */
self.manager.playTimeRecordEnabled = YES; // example:开启

/**
 视频的起始播放时间

 #discussion 需要在实例化 playerManager 对象后设置
 #discussion 设置为大于 0 的值时,记忆播放 playTimeRecordEnabled 无效
 #discussion !!!: 使用同一个 playerManager 播放不同视频时,该值不会自动重置,需要根据实际情况重新设置
 */
self.manager.initialPlayTime = 20.0; // example:设置起始播放时间为 20 秒

/** 
 集成方账户体系下的用户名及编号,用于上报,根据需要设置,需要在调用 setup video 初始化视频之前设置 */
self.manager.userName = @"student";
self.manager.userNumber = @"123456";

6. 添加播放视图

播放视图通过 BJVPlayerManagerplayerView 属性获取,布局自定。

6.1 添加视图

[self.videoViewController.view addSubview:self.playerManager.playerView];

6.2 布局示例:根据当前清晰度获取视频宽高比,使视频画面充满父视图 superView

bjl_weakify(self);
[self bjl_kvo:BJLMakeProperty(self.room.playerManager, currDefinitionInfo)
     observer:^BOOL(id  _Nullable now, id  _Nullable old, BJLPropertyChange * _Nullable change) {
         bjl_strongify(self);
         // 根据当前清晰度获取视频宽高比
         BJVDefinitionInfo *currDefinitionInfo = self.room.playerManager.currDefinitionInfo;
         CGFloat width = currDefinitionInfo.width;
         CGFloat height = currDefinitionInfo.height;
         CGFloat videoRatio = height > 0.0 ? width / height : 0.0;

         // 更新播放视图布局
         [self.playerManager.playerView bjl_remakeConstraints:^(BJLConstraintMaker *make) {
             if (videoRatio > 0) {
                 make.edges.equalTo(superview).priorityHigh();
                 make.center.equalTo(superview);
                 make.top.left.greaterThanOrEqualTo(superview);
                 make.bottom.right.lessThanOrEqualTo(superview);
                 make.width.equalTo(playerView.bjl_height).multipliedBy(videoRatio);
             }
             else {
                 make.edges.equalTo(superview);
             }
         }];

         return YES;
     }];

7. 初始化点播

  • 初始化在线点播
/**
 初始化在线视频,使用通用参数

 #param videoID 视频 ID
 #param token 需要集成方后端调用百家云后端的API获取,传给移动端
 #discussion 调用此方法初始化在线视频时,默认不加密、不使用集成方鉴权
 */
 [self.playerManager setupOnlineVideoWithID:videoID token:token];
/**
 初始化在线视频,设置是否加密、集成方鉴权

 #param videoID 视频 ID
 #param token 需要集成方后端调用百家云后端的API获取,传给移动端
 #param encrypted 是否加密,「仅在使用 IJKPlayer 时有效」,参考 initWithPlayerType: 方法
 #param accessKey 集成方鉴权,视频如果需要请求第三方服务器查看是否有权限,可设置该参数。鉴权验证请求需要与百家云后台沟通。
 */
[self.playerManager setupOnlineVideoWithID:videoID
                                     token:token 
                                 encrypted:encrypte
                                 accessKey:accessKey];
  • 初始化本地点播
/**
 初始化本地视频

 #param downloadItem  本地视频文件类型,通过下载模块获得
 */
[self setupLocalVideoWithDownloadItem:downloadItem];

8. 视频播放控制

播放器控制方法在 BJVPlayProtocol.h 文件中定义。

/** 播放 */
[self.playerManager play];
/** 暂停 */
[self.playerManager pause];
/**
 跳转时间

 #param time 目标时间,单位为秒
 */
[self.playerManager seek:time];
/**
 切换清晰度

 #param index 目标清晰度 在 BJVPlayInfo 的 definitionList 数组中的序号
 */
[self.playerManager changeDefinitionWithIndex:index];
/** 设置播放倍速,有效区间:[0.5, 2.0] */
self.playerManager.rate = 2.0;

9. 视频播放信息

视频播放信息在 BJVPlayProtocol.h 文件中定义

/** 当前播放状态 */
@property (nonatomic, readonly) BJVPlayerStatus playStatus;

// example:监听播放状态变化
bjl_weakify(self);
[self bjl_kvo:BJLMakeProperty(self.player, playerStatus) filter:^BOOL(NSNumber * _Nullable now, NSNumber * _Nullable old, BJLPropertyChange * _Nullable change) {
    return now.integerValue != old.integerValue;
} observer:^BOOL(NSNumber * _Nullable now, NSNumber * _Nullable old, BJLPropertyChange * _Nullable change) {
    bjl_strongify(self);
    BJVPlayerStatus status = now.integerValue;
    // 播放状态变化的自定义处理
    [self playerStatusDidChangeTo:status];
    return YES;
}];
/** 视频的时长 */
@property (nonatomic, readonly) NSTimeInterval duration;
/** 视频缓存时长 */
@property (nonatomic, readonly) NSTimeInterval cachedDuration;
/** 视频当前的播放时间 */
@property (nonatomic, readonly) NSTimeInterval currentTime;
/** 视频信息 */
@property (nonatomic, readonly, nullable) BJVPlayInfo *playInfo;
/** 当前播放清晰度 */
@property (nonatomic, readonly, nullable) BJVDefinitionInfo *currDefinitionInfo;

10. 视频播放中的错误监听

/*
 视频播放出错
 @param playInfo 出错视频信息
 @param error 错误信息
 */
- (BJLObservable)video:(BJVPlayInfo *)playInfo playFailedWithError:(NSError *)error;

// example:监听视频播放出错
[self bjl_observe:BJLMakeMethod(self.room.playerManager, video:playFailedWithError:) observer:^BOOL(BJVPlayInfo *playInfo, NSError *error) {
    bjl_strongify(self);
    // 自定义播放错误处理方法
    [self video:playInfo playFailedWithError:error];
    return YES;
}];
  • 错误码信息
typedef NS_ENUM(NSInteger, BJVErrorCode) {
    BJVErrorCode_unknown            = -999,     // 未知错误
    BJVErrorCode_requestFailed      = 1000,     // 网络请求失败
    BJVErrorCode_invalidToken       = 1010,     // token 参数错误
    BJVErrorCode_invalidPlayInfo    = 1020,     // 播放信息解析错误
    BJVErrorCode_invalidVideoURL    = 1030,     // 在线播放,视频地址失效
    BJVErrorCode_fileLost           = 1040,     // 本地播放,播放文件不存在
    BJVErrorCode_playFailed         = 1050      // 视频播放失败
};

11. 播放器清理、销毁

  • 清理

调用点播初始化方法 setupVideo 时将自动调用,其它情况可根据实际需要调用。

// 清理播放器,重置状态、清空数据
[self.playerManager reset];
  • 销毁
/**
 销毁播放器
 #discussion 调用后彻底销毁播放器,不可恢复
 #discussion 可在退出播放界面前调用
 */
[self.playerManager destroy];

12. 功能点说明

12.1 记忆播放

  • 设置 BJPlayerManagerplayTimeRecordEnabled 属性开关记忆播放。开启后记录 vid 对应的播放进度,每 5 秒【更新】一次播放进度,seek 时、停止播放时立即【更新】一次;
  • 如果 视频总时长 减去 当前进度 小于 N 秒,则视为播放结束,只移除记录、不添加,N 默认 5 秒,可自定义,参考BJPlayerManagerignorableRemainingTimeInterval 属性;
  • 调用 clearPlayTimeRecords 方法清除播放纪录;
  • 最多存 100 条,超过 100 条时顶掉最早的;
  • 设置视频初始播放时间为大于 0 的值时,记忆播放无效。

12.2 纯音频播放

  • 点播需要在后台配置转码纯音频才有纯音频
  • 播在线播放的默认清晰度可以在后台配置清晰度列表,SDK根据配置的清晰度优先级列表来匹配第一个清晰度,如果后台配置了纯音频最高优先级,就可以进入回放和点播的时候直接播放纯音频,否则就通过改变清晰度的方式播放纯音频
  • 视频清晰度PMVideoDefinitionType增加纯音频枚举
  • 纯音频播放
NSArray *definitionList = [self.playerManager.playInfo.definitionList copy];
for (NSInteger index = 0; index < definitionList.count; index ++) {
    BJVDefinitionInfo *definitionInfo = [[definitionList bjl_objectAtIndex:index] bjl_as:[BJVDefinitionInfo class]];
    if ([definitionInfo.definitionKey isEqualToString:@"audio"]) {
        [self.playerManager changeDefinitionWithIndex:index];
    }
}

12.3 视频淸晰度标识

BJVDefinitionInfodefinitionKey 表示清晰度标识符,与 definitionName 一一对应。可以随视频的实际清晰度动态扩展。

definitionKey definitionName
low 标清
high 高清
superHD 超清
720p 720p
1080p 1080p
audio 纯音频
... ...

回放功能集成

建议:自主集成回放功能之前,可参考 BJPlaybackUI SDK 已经开源的代码,该 UI SDK 提供了清晰的回放模块 API 调用方式。

1. 引入头文件

#import <BJVideoPlayerCore/BJVideoPlayerCore.h>

2. 设置专属域名前缀

参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

3. (可选)设置 BJVRequestTokenDelegate,管理 token

参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

4. 创建、进入回放房间

创建、进入回放房间的整体流程:

  • 按需要设置专属域名前缀
  • 定义一个 BJVRoom 的属性 room,用于管理回放房间
  • 使用回放相关信息将 room 属性实例化
  • 为回放的进入,退出,聊天消息更新等事件添加监听
  • 调用 roomenter 方法进入房间,监听到进入房间成功之后,即可开始观看回放

4.1 定义回放房间属性

@property (nonatomic) BJVRoom *room;

4.2 创建回放房间

  • 创建在线回放房间
/**
 创建在线回放房间,配置项采用默认值

 #discussion 具体参考 `onlinePlaybackRoomWithClassID:sessionID:token:encrypted:playerType:` 方法
 #discussion 视频不加密
 #discussion 使用 AVPlayer

 @param classID     课程 ID
 @param sessionID   课节 ID
 @param token       需要集成方后端调用百家云后端的API获取,传给移动端
 @return            回放房间实例
 */
self.room = [BJVRoom onlinePlaybackRoomWithClassID:classID
                                         sessionID:sessionID
                                             token:token];
/**
 创建在线回放房间,支持多项配置

 #param classID     课程 ID
 #param sessionID   课节 ID
 #param token       需要集成方后端调用百家云后端的API获取,传给移动端
 #param encrypted   是否加密,「仅在使用 IJKPlayer 时有效」,playerType 参数传 BJVPlayerType_IJKPlayer
 #param accessKey   集成方鉴权,回放如果需要请求第三方服务器查看是否有权限,可设置该参数。鉴权验证请求需要与百家云后台沟通。
 #param playerType  播放器类型:AVPlayer、IJKPlayer
 #return            回放房间实例
 */
self.room = [BJVRoom onlinePlaybackRoomWithClassID:classID
                                         sessionID:sessionID
                                             token:token
                                         encrypted:options.encryptEnabled
                                         accessKey:accessKey
                                        playerType:options.playerType];

  • 创建本地回放房间

创建本地回放的 downloadItem 参数通过下载功能获得

/**
 创建本地回放房间

 @param downloadItem    本地回放文件类型,通过下载模块获得
 @param playerType      播放器类型:AVPlayer、IJKPlayer
 @return                回放房间实例
 */
self.room = [BJVRoom localPlaybackRoomWithDownloadItem:downloadItem
                                            playerType:BJVPlayerType_AVPlayer];

4.3 回放房间状态监听

在创建回放房间之后、进入回放房间(调用 BJVRoomenter 方法)之前设置。

可参考 BJPlaybackUI SDKBJPRoomViewController+observer.m 文件的代码。

  • 监听进入房间
/**
 进入房间
 #param error 错误信息,成功进入时返回空值,错误码参考 BJVErrorCode
 */
- (BJLObservable)roomDidEnterWithError:(nullable NSError *)error;

// 示例代码
bjl_weakify(self);
[self bjl_observe:BJLMakeMethod(self.room, roomDidEnterWithError:)
             observer:^BOOL(BJLError *error) {
                 // 此处建议在监听到进入房间成功之后,再对 BJVRoom 的 view models进行监听
                 bjl_strongify(self);
                 // example: 添加对 BJVRoom 的各个 VM 的监听
                 [self addObserversForPlaybackViewModels];
                 // example: 添加对 BJVRoom 的播放器的相关监听
                 [self addObserversForVideoPlayer];
                 return YES;
             }];
  • 监听退出房间
/**
 退出房间
 #param error 错误信息,成功退出时返回空值,错误码参考 BJVErrorCode
 */
- (BJLObservable)roomDidExitWithError:(NSError *)error;

// 示例代码
bjl_weakify(self);
[self bjl_observe:BJLMakeMethod(self.room, roomDidExitWithError:)
             observer:^BOOL(BJLError *error) {
                 bjl_strongify(self);
                 // 退出房间之后的自定义操作
                 [self playbackEndWithError:error];
                 return YES;
             }];

  • 监听加载状态
/**
 是否正在加载
 */
@property (nonatomic, readonly) BOOL loading;

// 示例代码
bjl_weakify(self);
[self bjl_kvo:BJLMakeProperty(self.room, loading)
     observer:^BOOL(NSNumber * _Nullable value, NSNumber * _Nullable oldValue, BJLPropertyChange * _Nullable change) {
         bjl_strongify(self);
         // example: 根据 loading 值显示/隐藏加载动画
         if (value.boolValue) {
             [self showLoading];
         }
         else {
             [self stopLoading];
         }
         return YES;
     }];
  • 错误码信息
typedef NS_ENUM(NSInteger, BJVErrorCode) {
    BJVErrorCode_unknown            = -999,     // 未知错误
    BJVErrorCode_requestFailed      = 1000,     // 网络请求失败
    BJVErrorCode_invalidToken       = 1010,     // token 参数错误
    BJVErrorCode_invalidPlayInfo    = 1020,     // 播放信息解析错误
    BJVErrorCode_invalidVideoURL    = 1030,     // 在线播放,视频地址失效
    BJVErrorCode_fileLost           = 1040,     // 本地播放,播放文件不存在
    BJVErrorCode_playFailed         = 1050      // 视频播放失败
};

5. 进入、退出、刷新回放房间

/**  进入房间 */
- (void)enter;

/** 退出房间 */
- (void)exit;

/** 重新加载房间内容
 #discussion 用于回放过程中出错后重试。
 */
- (void)reload;

6. 回放视频播放管理

与点播的 BJVPlayerManager 类似,回放房间 BJVRoomplayerManager 遵循 BJVPlayProtocol 协议,因此回放的视频播放管理可以参考点播的播放设置添加播放视图视频播放控制视频播放信息视频播放中的错误监听 等部分,使用 self.room.playerManager 调用相关方法。

注意:调用 self.room.playerManagerdestroy 方法将导致回放房间不可用,且不可恢复。

7. 回放课件管理

回放房间课件的白板、PPT、画笔的展示都来自 BJVRoomslideshowViewController 用户根据需要将slideshowViewController.view添加到需要展示的 view 上即可。

7.1 课件设置

  • 交互设置:为保证课件内容与视频同步,需要设置回放的课件不支持交互
self.room.slideshowViewController.view.userInteractionEnabled = NO;
  • 显示设置:通过设置 BJLSlideshowUI 中的相关属性设置,回放的课件均为静态课件
/** 静态课件显示模式
 只支持 BJLContentMode_scaleAspectFit、BJLContentMode_scaleAspectFill
 只对静态课件生效,参考 `BJLRoom` 的 `disablePPTAnimation`
 */
@property (nonatomic) BJLContentMode contentMode;

/** 静态课件尺寸
 加载课件图片时对图片做等比缩放,长边小于/等于 `imageSize`,放大时加载 1.5 倍尺寸的图片
 单位为像素,默认初始加载 720、放大加载 1080,取值在 `BJLAliIMGMinSize` 到 `BJLAliIMGMaxSize` 之间 (1 ~ 4096)
 不建议进教室成功后设置此参数,因为会导致已经加载过的图片缓存失效
 只对静态课件生效,参考 `BJLRoom` 的 `disablePPTAnimation`
 */
@property (nonatomic) NSInteger imageSize;

/** 静态课件占位图
 只对静态课件生效,参考 `BJLRoom` 的 `disablePPTAnimation`
 */
@property (nonatomic) UIImage *placeholderImage;
  • 课件动效设置(仅支持在线回放)
    • BJVRoomdisablePPTAnimation 属性表示是否禁止课件动效,在线回放默认禁止,可设置为 NO 以开启动效;
    • 本地回放不支持课件动效。

7.2 课件显示

// example:添加课件视图到当前的视图控制器
UIViewController *pptViewController = self.room.slideshowViewController;
[self addChildViewController:pptViewController];
[self.view addSubview:pptViewController.view];
[pptViewController didMoveToParentViewController:self];

8. 回放房间信息

回放房间信息由 BJVRoomVM 管理,在 BJVRoom 的实例创建成功之后,可以对 self.room.roomVM 添加监听。

/** 回放房间公告 */
@property (nonatomic, readonly) BJVNotice *notice;

/** 当前是否正在播放媒体文件 */
@property (nonatomic, readonly) BOOL isMediaPlaying;

/** 当前是否正在共享屏幕 */
@property (nonatomic, readonly) BOOL isDesktopSharing;

9. 回放聊天消息

9.1 聊天消息监听

回放聊天消息由 BJVMessageVM 管理,在 BJVRoom 的实例创建成功之后,可以对 self.room.messageVM 添加监听。

Core SDK 不维护聊天消息的列表,而是通过 消息覆盖更新、增量更新 的回调通知 UI 进行维护。

  • 消息覆盖更新,重置列表
// 消息覆盖更新
- (BJLObservable)receivedMessagesDidOverwrite:(nullable NSArray<BJVMessage *> *)messages;

// example:监听消息覆盖更新
bjl_weakify(self);
[self bjl_observe:BJLMakeMethod(room.messageVM, receivedMessagesDidOverwrite:)
         observer:^BOOL(NSArray<BJVMessage *> *messageArray){
             bjl_strongify(self);
             // !!!: 回放消息数有限,且 key 是唯一的,重置列表时不需要清除高度缓存
             self.allMessages = [messageArray mutableCopy];
             [self.tableView reloadData];
             return YES;
         }];
  • 消息增量更新,扩展列表
// 消息增量更新
- (BJLObservable)didReceiveMessages:(nullable NSArray<BJVMessage *> *)messages;

// example:监听消息增量更新
bjl_weakify(self);
[self bjl_observe:BJLMakeMethod(room.messageVM, didReceiveMessages:)
             observer:^BOOL(NSArray<BJVMessage *> *messageArray){
                 bjl_strongify(self);
                 if (!messageArray.count) {
                     return YES;
                 }
                 [self.allMessages addObjectsFromArray:messageArray];
                 [self.tableView reloadData];
                 return YES;
             }];

9.2 (重要)聊天列表界面性能优化

显示聊天消息的 UI 性能是需要重点优化的,大幅跳转回放视频进度会导致聊天数据的大量更新,此时回放聊天界面性能消耗过高会导致 UI 卡顿、音视频和课件不同步,严重时会卡死主线程。优化方式主要有:使用高度缓存、控制列表刷新频率、分页加载等等。具体实现可以参考 BJPlaybackUI SDKBJPChatMessageViewController.m 文件。如果成功优化之后依旧存在卡顿,可以使用 profile 调试工具针对性能消耗较大的功能进行针对性的优化。

10. 用户列表

回放用户列表由 BJVOnlineUserVM 管理,在 BJVRoom 的实例创建成功之后,可以对 self.room.onlineUsersVM 添加监听。

  • 房间用户列表

注意:长期课程的回放,除第1节之外的后续课节,房间用户列表信息不准确

/** 在线人数 */
@property (nonatomic, readonly) NSInteger onlineUsersTotalCount;

/** 在线用户 */
@property (nonatomic, readonly, nullable, copy) NSArray<BJVUser *> *onlineUsers;
// example:监听房间用户列表的变化
bjl_weakify(self);
[self bjl_kvo:BJLMakeProperty(self.room.onlineUsersVM, onlineUsers)
     observer:^BOOL(id  _Nullable now, id  _Nullable old, BJLPropertyChange * _Nullable change) {
         bjl_strongify(self);
         self.userList = [self.room.onlineUsersVM.onlineUsers copy];
         [self.tableView reloadData];
         return YES;
    }];
  • 用户进出房间
/** 有用户进入房间
 同时更新 `onlineUsers` */
- (BJLObservable)onlineUsersDidEnter:(NSArray<BJVUser *> *)users;

/** 有用户退出房间
 同时更新 `onlineUsers` */
- (BJLObservable)onlineUsersDidExit:(NSArray<BJVUser *> *)users;
  • 当前主讲人
/** 当前主讲 */
@property (nonatomic, readonly, nullable, copy) BJVUser *currentPresenter;
// example:监听房间主讲人变化
bjl_weakify(self);    
[self bjl_kvo:BJLMakeProperty(self.room.onlineUsersVM, currentPresenter)
     observer:^BOOL(id  _Nullable now, id  _Nullable old, BJLPropertyChange * _Nullable change) {
         bjl_strongify(self);
         BJVUser *presenter = self.room.onlineUsersVM.currentPresenter;
         NSLog(@"当前主讲为:%@", presenter.name);
         return YES;
     }];
  • 发言用户列表变化
/**
 音视频用户列表变更
 @param mediaUsers 音视频用户列表
 */
- (BJLObservable)mediaUsersDidUpdate:(NSArray<BJVUser *> *)mediaUsers;
// example:监听房间发言用户列表的变化
bjl_weakify(self);
[self bjl_observe:BJLMakeMethod(self.room.onlineUsersVM, mediaUsersDidUpdate:)
         observer:^BOOL(NSArray<BJVUser *> *mediaUsers) {
             bjl_strongify(self);
             self.mediaUsers = [mediaUsers copy];
             [self.tableView reloadData];
             return YES;
         }];

下载功能集成

建议: 自主集成下载功能之前,可参考 BJVideoPlayerUI SDK demo 的代码,该 demo 提供了清晰的下载模块 API 调用方式。

1. 功能点说明

1.1 后台下载

  1. 2.x 的下载通过NSURLSession 实现,支持后台下载 —— App 被杀死之后在电量充足、接入 Wi-Fi 的情况下仍可继续完成下载,参考苹果官方关于后台下载的文档

  2. 由于 NSURLSession 限制,不建议做下载队列管理,等待中的队列实际上是被暂停,后台下载会失效,建议使用priority属性设置下载任务的优先级;

  3. 对于 NSURLSession 的设置,例如是否支持 4G 下载等,需要在 application:will/didFinishLaunchingWithOptions:第一时间 设置 BJLDownloadManagerclassDelegate 属性,并实现 BJLDownloadManagerClassDelegate 中定义的方法;

  4. 如果 AppDelegate 中实现了这个方法 -handleEventsForBackgroundURLSession:completionHandler:,需要在方法的实现中调用 BJLDownloadManager 的 +handleEventsForBackgroundURLSession:completionHandler:,否则后台下载任务无法正常恢复;

2. 引入头文件

#import <BJVideoPlayerCore/BJVideoPlayerCore.h>

3. 设置专属域名

参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

4. (可选)设置 BJVRequestTokenDelegate,管理 token

参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

5. 初始化下载管理类 BJVDownloadManager

BJVDownloadManager的具体用法可以参考 BJVideoPlayerUI SDK demoBJVDownloadViewController.m 文件。

5.1 定义下载 manager 属性

@property (nonatomic) BJVDownloadManager *downloadManager;

5.2 创建 manager 实例

5.2.1 初始化

创建BJVDownloadManager支持设置多个参数,identifier是每个实例的唯一标识,可传入集成方的用户账号,用来区分不同账号的下载数据inCaches 可以让下载文件保存在 caches 目录(系统可能会自动清理下载文件,但会保留下载记录,支持重新下载文件),参数默认为 NO。

// 指定标识,不保存在 cache 目录
+ (instancetype)downloadManagerWithIdentifier:(NSString *)identifier;

// 指定标识、配置是否保存在 cache 目录
+ (instancetype)downloadManagerWithIdentifier:(NSString *)identifier inCaches:(BOOL)inCaches;

// example
self.manager = [BJVDownloadManager downloadManagerWithIdentifier:@"user.identifier" inCaches:NO];
5.2.2 下载 NSURLSessionConfiguration 配置

对于 NSURLSession 的设置,例如是否支持 4G 下载等,需要:

  • application:will/didFinishLaunchingWithOptions:第一时间 设置 BJLDownloadManagerclassDelegate 属性
// 设置 Class 代理
+ (nullable id<BJLDownloadManagerClassDelegate>)classDelegate;
+ (void)setClassDelegate:(nullable id<BJLDownloadManagerClassDelegate>)classDelegate;

// example
[BJVDownloadManger setClassDelegate:self];
  • 实现 BJLDownloadManagerClassDelegate 协议中的 downloadManager:URLSessionConfiguration: 方法,在该方法中更改 NSURLSessionConfiguration 属性
//  Class 代理方法示例
- (void)downloadManager:(BJLDownloadManager *)downloadManager URLSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    // 在这里根据需要修改 configuration 的配置项
    // configuration.allowsCellularAccess = YES; // The default value is YES.
    // configuration.HTTPMaximumConnectionsPerHost = <#MAX#>; // The default value is 6 in macOS, or 4 in iOS.
}

程序运行过程中更改 NSURLSession 的设置,需要:

  • 调用 BJLDownloadManagerrefreshURLSessionWithCompletion: 方法,注意refreshURLSessionWithCompletion: 方法回调 completion 之前不要对下载任务进行 addremovepauseresume 等操作
[self openLoadingView];
// 开始刷新 NSURLSession
[self.downloadManager refreshURLSessionWithCompletion:^(BJLDownloadManager * _Nonnull downloadManager) {
    // 刷新 NSURLSession 完成
    [self closeLoadingView]; // userInteractionEnabled: NO
}];
  • 然后在 downloadManager:URLSessionConfiguration: 方法中给 NSURLSessionConfiguration 的属性设置不同的值

6. 下载任务管理

下载任务由 BJVDownloadManager 定义的相关方法管理。

6.1 校验下载任务是否可添加

注意:视频下载之前都需要先调用 validateItemWithVideoID: 或者validateItemWithClassID:sessionID: 校验点播/回放是否可以下载。

/**
 校验下载任务对象是否可以添加

 #return 是否可添加
 #discussion 已经添加过/已下载完成 的任务,方法返回 NO
 */
- (BOOL)validateItemWithVideoID:(NSString *)videoID;
- (BOOL)validateItemWithClassID:(NSString *)classID sessionID:(nullable NSString *)sessionID;

// example: 判断点播视频是否可下载
BOOL validate = [self.downloadManager validateItemWithVideoID:videoID];

// example: 判断回放是否可下载
BOOL validate = [self.downloadManager validateItemWithClassID:classID sessionID:sessionID];

6.2 添加下载任务

  • 添加点播下载任务
/**
 点播
 #param videoID      视频 ID
 #param encrypted    是否加密
 #param preferredDefinitionList 下载清晰度列表,按顺序匹配、没有匹配将导致下载失败,传 nil 使用默认清晰度
 #discussion preferredDefinitionList 列表元素为清晰度的标识字符串,现有标识符:low(标清),high(高清),superHD(超清),720p,1080p,audio(纯音频),可根据实际情况动态扩展
 #param setting      添加成功后用于设置 item 属性的回调
 #return 添加成功返回 BJVDownloadItem 实例,videoID 已存在会返回 nil
 */
- (nullable BJVDownloadItem *)addDownloadItemWithVideoID:(NSString *)videoID
                                               encrypted:(BOOL)encrypted
                                 preferredDefinitionList:(nullable NSArray<NSString *> *)preferredDefinitionList;
- (nullable BJVDownloadItem *)addDownloadItemWithVideoID:(NSString *)videoID
                                               encrypted:(BOOL)encrypted
                                 preferredDefinitionList:(nullable NSArray<NSString *> *)preferredDefinitionList
                                                 setting:(nullable void (^)(BJVDownloadItem *item))setting;                                              
// 示例代码
if (![self.downloadManager validateItemWithVideoID:videoID]) {
    NSLog(@"video existed: videoID %@", videoID);
    return;
}
BJVDownloadItem *downloadItem =
[self.downloadManager addDownloadItemWithVideoID:videoID
                                       encrypted:encrypted
                         preferredDefinitionList:preferredDefinitionList
                                         setting:^(BJVDownloadItem * _Nonnull item) {
                                             // 集成方鉴权,根据需要设置
                                             item.accessKey = accessKey;
                                             // 集成方自定义信息
                                             item.userInfo = userInfo;
                                         }];
  • 添加回放下载任务
/**
 回放
 #param classID      课程 ID
 #param sessionID    课节 I
 #param encrypted    是否加密
 #param preferredDefinitionList  下载清晰度列表,列表元素为 NSNumber<BJVDefinitionType> *,按顺序匹配、没有匹配将导致下载失败,传 nil 使用默认清晰度
 #discussion preferredDefinitionList 列表元素为清晰度的标识字符串,现有标识符:low(标清),high(高清),superHD(超清),720p,1080p,audio(纯音频),可根据实际情况动态扩展
 #param setting      添加成功后用于设置 item 属性的回调
 #return 添加成功返回 BJVDownloadItem 实例,classID+sessionID 已存在会返回 nil
 */
- (nullable BJVDownloadItem *)addDownloadItemWithClassID:(NSString *)classID
                                               sessionID:(nullable NSString *)sessionID
                                               encrypted:(BOOL)encrypted
                                 preferredDefinitionList:(nullable NSArray<NSString *> *)preferredDefinitionList;
- (nullable BJVDownloadItem *)addDownloadItemWithClassID:(NSString *)classID
                                               sessionID:(nullable NSString *)sessionID
                                               encrypted:(BOOL)encrypted
                                 preferredDefinitionList:(nullable NSArray<NSString *> *)preferredDefinitionList
                                                 setting:(nullable void (^)(BJVDownloadItem *item))setting;
// 示例代码
if (![self.manager validateItemWithClassID:classID sessionID:sessionID]) {
    NSLog(@"playback existed: %@-%@", classID, sessionID);
    return;
}
BJVDownloadItem *downloadItem =
[self.downloadManager addDownloadItemWithClassID:classID
                                       sessionID:sessionID
                                       encrypted:encrypted
                            preferredDefinitionList:preferredDefinitionList
                                            setting:^(BJVDownloadItem * _Nonnull item) {
                                                // 集成方鉴权,根据需要设置
                                                item.accessKey = accessKey;
                                                // 集成方自定义信息
                                                item.userInfo = userInfo;
                                            }];

6.3 纯音频下载

同一个视频只能下载纯音频或其他的一种清晰度,按照传入的清晰度列表匹配第一个满足条件的清晰度。下载纯音频时,调用上面添加下载任务方法时, preferredDefinitionList 参数传入 @[@"audio"] 即可。

7. 获取下载任务列表

  • 所有已添加的下载任务,包括下载中、下载完成和下载失败的
@property (nonatomic, readonly, copy) NSArray<__kindof BJLDownloadItem *> *downloadItems;

// example: 获取所有下载任务
NSArray *allDownloadItems = self.downloadManager.downloadItems;
  • 获取不同下载状态的下载任务

注意: downloadItemsWithStates:方法的参数一定要以NSNotFound结束

// !!!: REQUIRES NSNotFound TERMINATION
- (nullable NSMutableArray<__kindof BJLDownloadItem *> *)downloadItemsWithStates:(BJLDownloadItemState)state, ...; 

// example:获取下载中的任务
NSArray<BJVDownloadItem *> *items = [self.downloadManager downloadItemsWithStates:BJLDownloadItemState_running, NSNotFound];

8. 下载任务数据类型 BJVDownloadItem

添加下载任务方法返回值为 BJVDownloadItem 实例,对应单个下载任务,包含下载任务的各项信息、状态等。

8.1 任务信息

/** 点播视频 ID*/
@property (nonatomic, readonly, nullable) NSString *videoID;
/** 回放课程、课节 ID*/
@property (nonatomic, readonly, nullable) NSString *classID, *sessionID;
/** 是否为回放 item*/
@property (nonatomic, readonly) BOOL isPlayback;
/** 文件是否加密*/
@property (nonatomic, readonly) BOOL isEncrypted;
/** 视频播放信息*/
@property (nonatomic, readonly, nullable) BJVPlayInfo *playInfo;
/** 视频、信令文件 */
@property (nonatomic, readonly, nullable) BJLDownloadFile *videoFile, *signalFile;
/** 封面、水印图片*/
@property (nonatomic, readonly, nullable) BJLDownloadFile *coverImageFile, *watermarkImageFile;
/** 视频清晰度*/
@property (nonatomic, readonly) BJVDefinitionInfo *currentDefinitionInfo;

8.2 监听下载任务信息变化

对于每一个下载任务,可从 BJVDownloadItem 的属性读取下载相关的信息。

属性对应关系如下:

属性名 含义
state 下载状态,参考 `BJLDownloadItemState`
priority 下载优先级,取值范围 0.0 - 1.0,参考 `NSURLSessionTask.priority`
progress 下载进度,progress.fractionCompleted 取值范围 0.0 - 1.0,表示进度百分比
downloadFiles 下载文件数组
totalSize 下载文件总大小单位字节
bytesPerSecond 下载速度,单位字节每秒
coverImageFile 封面图片
8.2.1 监听 BJVDownloadItem 变化更新下载列表 UI
  • 设置 BJLDownloadManagerdelegate 属性
// 设置代理
@property (nonatomic, weak, nullable) id<BJLDownloadManagerDelegate> delegate;

// example
self.downloadManger.delegate = self;
  • 实现 BJLDownloadManagerDelegate 协议中的 downloadManager:downloadItem:didChange: 方法,在该方法中更新视图
- (void)downloadManager:(BJVDownloadManager *)downloadManager
           downloadItem:(BJVDownloadItem *)downloadItem
              didChange:(BJLPropertyChange *)change {
    NSInteger index = [self.downloadManager.downloadItems indexOfObject:downloadItem];
    if (index == NSNotFound) {
        return;
    }

    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
    BJVDownloadItemCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
    [cell updateWithItem:downloadItem];
}
8.2.2 显示下载进度和速度

由于下载进度的变化过于频繁,并且不在主线程回调,需要切换到主线程才能更新 UI,因此会严重影响渲染帧率,多个任务同时下载时更加严重。

另外,当进度没有变化时速度需要用 timer 来实现衰减的效果,但是每个 BJVDownloadItem 内置一个 timer 就有点浪费了。

因此

  • 使用一个全局的 timer 定时、主动获取下载进度和速度,并且只在必要时才去一次性的重新渲染视图
    bjl_weakify(self);
    self.progressTimer = [NSTimer bjl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        // self dealloc 后销毁 timer
        bjl_strongify_ifNil(self) {
            [timer invalidate];
            return;
        }
        // 视图不显示时跳过
        if (!self.isViewLoaded || !self.view.window) {
            return;
        }
        // 只刷新可见 cell
        NSArray<NSIndexPath *> *indexPaths = [self.tableView indexPathsForVisibleRows];
        for (NSIndexPath *indexPath in indexPaths) {
            BJVDownloadItem *item = [self.downloadManager.downloadItems bjl_objectAtIndex:indexPath.row];
            BJVDownloadItemCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            // 尝试更新
            [cell tryToUpdateWithItem:item];
        }
    }];
    // 避免滑动时 timer 回调不执行
    [[NSRunLoop currentRunLoop] addTimer:self.progressTimer forMode:NSRunLoopCommonModes];
  • UITableViewCell 中缓存上一次下载进度和速度,在 timer 执行时检查变化,只有变化足够大时才更新视图
// 缓存进度和速度
@property (nonatomic) BJLProgress cachedProgress;
@property (nonatomic) long long cachedBytesPerSecond;

- (void)prepareForReuse {
    [super prepareForReuse];
    // prepare 时重置缓存
    self.cachedProgress = BJLProgressMake(0, 0);
    self.cachedBytesPerSecond = - 1;
}

- (void)tryToUpdateWithItem:(BJVDownloadItem *)item {
    // 尝试更新时与缓存比较,这里 `progress.fractionCompleted` 变化达到 0.1% 或者下载速度有变化时才会更新视图
    if ([item compareWithProgress:self.cachedProgress bytesPerSecond:self.cachedBytesPerSecond]) {
        [self updateWithItem:item];
    }
}

- (void)updateWithItem:(BJVDownloadItem *)item {
    // 更新视图时更新缓存
    self.cachedProgress = item.progress;
    self.cachedBytesPerSecond = item.bytesPerSecond;
    // 更新视图
    ...
}

8.3 下载状态说明

需要注意的是:在拿到BJVDownloadItem更新UI时,需要结合BJVDownloadItemstateerror对当前下载任务作出正确的判断。

  • BJLDownloadItemState_paused && error 表示下载中出错
  • BJLDownloadItemState_completed && error 表示下载完文件丢失
typedef NS_ENUM(NSInteger, BJLDownloadItemState) {
    BJLDownloadItemState_invalid = - 1, // NOT be added to any BJLDownloadManager, or be invalidated 
    BJLDownloadItemState_running, 
    BJLDownloadItemState_paused, // paused + error = error occurred 
    BJLDownloadItemState_completed, // completed + error = file lost
    BJLDownloadItemState_suspended = BJLDownloadItemState_paused // 与 BJLDownloadItemState_paused 等效,将废弃
};

9. 下载任务控制

  • 开始:调用 BJVDownloadItemresume 方法
[item resume];
  • 暂停:调用 BJVDownloadItempause 方法

pause 方法用于替代 suspend (suspend 现在仍然可用),解决了频繁调用 suspendresume 可能会出现的下载进度不动的问题,因此 restart 方法也不需要了

[item pause];
  • 删除:调用 BJVDownloadManagerremoveDownloadItemWithIdentifier: 方法
[self.downloadManager removeDownloadItemWithIdentifier:item.itemIdentifier];

10. 旧版点播回放 SDK 下载数据迁移

PMDownloadManager 下载数据迁移,可参考 BJVideoPlayerUI SDK demo 中的 BJVDownloadViewController.m 文件

  • 规则:

    • 未下载完成的任务 - 已下载部分数据只能丢弃,创建新的下载任务重新下载;

    • 已下载完成的任务 - 已下载文件将被保留,迁移完成后需要通过网络请求获取新下载记录所需的基本信息;

  • 步骤:

    • !!!: 迁移过程需要访问网络,但网络请求可能因各种原始而失败、App 也有可能因各种原因被杀死或崩溃,因此需要设计一个入口、能让用户随时触发迁移,确保迁移成功才关闭入口;
      1. 实现 协议,可根据视频 videoIDclassIDsessionID 返回相应的 token
      1. App 启动后调用 +[PMDownloadManager downloadManagerWithRootPath:] 设置旧的下载根目录,然后调用 PMDownloadManager.downloadingListPMDownloadManager.finishedList 分别获取下载中和下载完成的任务 - PMDownloadModel 的实例;
      1. 如果 PMDownloadModel.readyForMigration 是 NO,需要调用 [PMDownloadModel prepareForMigrationWithCompletion:] 完成迁移前准备;
      1. 对每个任务调用 BJLDownloadManagermigratePMDownloadModel:code:migrateCompletedPMDownloadModel:code: 方法进行迁移;
      1. 迁移方法返回的 downloadItem 不为 nil、且 code 为 BJVDownloadMigrationCode_added 时表示添加成功;
      1. 添加成功后状态为 paused,需要 resume 来补齐视频/回放的基本信息(autoResume 设置 YES,或调用 [BJVDownloadItem resume]),此步骤需要联网完成,出错可通过 BJVDownloadItem.error 查看,再次调用 [BJVDownloadItem resume] 可重试;
      1. 迁移完成后调用 [PMDownloadManager removePMDownloadModel:] 方法删除旧的下载记录。

10.1 (必须)设置 BJVRequestTokenDelegate,管理 token

SDK 需要旧版数据对应的 token 才能完成迁移,因此必须实现 BJVRequestTokenDelegate 中的方法:参考全局设置部分,点播、回放、下载通用,项目工程内只需要设置一次

10.2 使用旧版数据存放路径创建下载管理类 PMDownloadManager 实例

可参考 BJVideoPlayerUI SDK demo 中的 PUAppDelegate.m 文件

PMDownloadManager *pmDownloadManager = [PMDownloadManager downloadManagerWithRootPath:yourPath];

10.3 获取旧版 SDK 下载数据

  • 获取下载中的旧数据列表
NSArray <PMDownloadModel *> *downloadingModels = pmDownloadManager.downloadingList;
  • 获取下载完成的旧数据列表
NSArray <PMDownloadModel *> *finishedModels = pmDownloadManager.finishedList;

10.4 准备迁移

如果 PMDownloadModelreadyForMigration 是 NO,需要调用 prepareForMigrationWithCompletion: 完成迁移前准备。

// example: 迁移旧版的 `PMDownloadModel` 实例 `model` 到新版下载
// 实例可从上面获取的 `pmDownloadManager.downloadingList` 和 `pmDownloadManager.finishedList` 中获取
BOOL completed = (model.state == PMDownloadState_Completed);
if (completed && !model.readyForMigration) {
    bjl_weakify(self);
    [model prepareForMigrationWithCompletion:^(NSError * _Nullable error) {
        bjl_strongify(self);
        if (error || !model.readyForMigration) {
            [self showHUDWithMessage:[NSString stringWithFormat:@"prepare error - %@: %@", model.videoFileName, error]];
        }
        else {
            // 准备完成,开始迁移
            [self migrateDownloadModel:model];
        }
    }];
    return;
}

10.5 开始迁移旧版下载数据

下载中的 model 调用 [BJLDownloadManager migratePMDownloadModel:code:] 方法迁移;下载完成的 model 调用 [BJLDownloadManager migrateCompletedPMDownloadModel:code:] 方法进行迁移。

迁移方法返回的 downloadItem 不为 nil、且 code 为 BJVDownloadMigrationCode_added 时表示添加成功;否则抛出错误,错误码参考 BJVDownloadMigrationCode

// example:model 为 `PMDownloadModel` 实例
BJVDownloadMigrationCode code = BJVDownloadMigrationCode_added;
BJVDownloadItem *item = nil;
BOOL completed = (model.state == PMDownloadState_Completed);
if (!completed) {
    item = [self.manager migratePMDownloadModel:model code:&code];
}
else {
    item = [self.manager migrateCompletedPMDownloadModel:model code:&code];
}

10.6 旧版下载数据迁移结果处理

在上面 开始迁移旧版数据 的示例代码中,调用 BJLDownloadManagermigratePMDownloadModel:code:migrateCompletedPMDownloadModel:code: 方法将返回 BJVDownloadItem 类型的新版下载数据实例 item,并给 code 赋值。通过 itemcode 判断迁移结果。

// example:开始迁移 -> 处理迁移结果 的完整示例,`model` 为 `PMDownloadModel` 实例 
BJVDownloadMigrationCode code = BJVDownloadMigrationCode_added;
BJVDownloadItem *item = nil;

if (!completed) {
    item = [self.manager migratePMDownloadModel:model code:&code];
}
else {
    item = [self.manager migrateCompletedPMDownloadModel:model code:&code];
}

if (item) {
    [self observeItem:item];
    [self showHUDWithMessage:@"添加成功"];
    [self.tableView reloadData];
}
else {
    switch (code) {
        case BJVDownloadMigrationCode_ivalidPMDownloadModel:
            [self showHUDWithMessage:[NSString stringWithFormat:@"ivalidPMDownloadModel - %@", model.videoFileName]];
            break;
        case BJVDownloadMigrationCode_uncompletedPMDownloadModel:
            [self showHUDWithMessage:[NSString stringWithFormat:@"uncompletedPMDownloadModel - %@", model.videoFileName]];
            break;
        case BJVDownloadMigrationCode_fileLost:
            [self showHUDWithMessage:[NSString stringWithFormat:@"fileLost - %@", model.videoFileName]];
            break;
        case BJVDownloadMigrationCode_notReadyForMigration:
            [self showHUDWithMessage:[NSString stringWithFormat:@"notReadyForMigration - %@", model.videoFileName]];
            break;
        case BJVDownloadMigrationCode_failedWithError:
            [self showHUDWithMessage:[NSString stringWithFormat:@"failedWithError - %@", model.videoFileName]];
            break;
        default:
            [self showHUDWithMessage:[NSString stringWithFormat:@"unknownError - %@", model.videoFileName]];
            break;
    }
}

10.7 补全新版下载数据

旧版下载数据迁移成功后获得的 BJVDownloadItem 实例 item 的状态为 paused,需要通过 resume 来补齐视频/回放的基本信息(设置 BJVDownloadManagerautoResume 为 YES,或调用 [item resume]),此步骤需要联网完成,出错可通过监听 item.error 查看 (参考监听下载任务信息变化),再次调用 [item resume] 可重试。

[item resume];

或者

self.downloadManager.autoResume = YES;

10.8 迁移成功后删除旧版数据

迁移成功后调用 PMDownloadManagerremovePMDownloadModel: 方法删除旧的下载记录。迁移失败时建议保留数据,方便重试。

// example:pmDownloadManager 由 9.2 部分的示例代码获取;model 由 9.3 部分的示例代码获取
[pmDownloadManager removePMDownloadModel:model];