ZFPlayerControl内核替换为阿里云点播

背景简介

一个使用ZFPlayerControl的项目需要切换阿里云播放器,这是一个很有趣的替换实现,以一个最小的代价去替换实现快速上线。

GitHub地址

git@github.com:iamanthonyzhu/ZFPlayerCoreAli.git
fork ZFPlayer实现该目的

原理实现

ZFPlayer

ZFPlayer是一个通用的视频框架,而他使用的是iOS avPlayer内核。

ControlView

该框架基本就是controlView+avPlayer的视频播放框架.
框架的controlView遵循ZFPlayerMediaControl协议

@protocol ZFPlayerMediaControl <NSObject>

@required
/// Current playerController
@property (nonatomic, weak) ZFPlayerController *player;

@optional

#pragma mark - Playback state

/// When the player prepare to play the video.
- (void)videoPlayer:(ZFPlayerController *)videoPlayer prepareToPlay:(NSURL *)assetURL;

/// When th player playback state changed.
- (void)videoPlayer:(ZFPlayerController *)videoPlayer playStateChanged:(ZFPlayerPlaybackState)state;

/// When th player loading state changed.
- (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state;

#pragma mark - progress

/**
 When the playback changed.
 
 @param videoPlayer the player.
 @param currentTime the current play time.
 @param totalTime the video total time.
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
        currentTime:(NSTimeInterval)currentTime
          totalTime:(NSTimeInterval)totalTime;

/**
 When buffer progress changed.
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
         bufferTime:(NSTimeInterval)bufferTime;

/**
 When you are dragging to change the video progress.
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
       draggingTime:(NSTimeInterval)seekTime
          totalTime:(NSTimeInterval)totalTime;

/**
 When play end.
 */
- (void)videoPlayerPlayEnd:(ZFPlayerController *)videoPlayer;

/**
 When play failed.
 */
- (void)videoPlayerPlayFailed:(ZFPlayerController *)videoPlayer error:(id)error;

#pragma mark - lock screen

/**
 When set `videoPlayer.lockedScreen`.
 */
- (void)lockedVideoPlayer:(ZFPlayerController *)videoPlayer lockedScreen:(BOOL)locked;

#pragma mark - Screen rotation

/**
 When the fullScreen maode will changed.
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationWillChange:(ZFOrientationObserver *)observer;

/**
 When the fullScreen maode did changed.
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationDidChanged:(ZFOrientationObserver *)observer;

#pragma mark - The network changed

/**
 When the network changed
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer reachabilityChanged:(ZFReachabilityStatus)status;

#pragma mark - The video size changed

/**
 When the video size changed
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer presentationSizeChanged:(CGSize)size;

#pragma mark - Gesture

/**
 When the gesture condition
 */
- (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl
                    gestureType:(ZFPlayerGestureType)gestureType
              gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
                          touch:(UITouch *)touch;

/**
 When the gesture single tapped
 */
- (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl;

/**
 When the gesture double tapped
 */
- (void)gestureDoubleTapped:(ZFPlayerGestureControl *)gestureControl;

/**
 When the gesture begin panGesture
 */
- (void)gestureBeganPan:(ZFPlayerGestureControl *)gestureControl
           panDirection:(ZFPanDirection)direction
            panLocation:(ZFPanLocation)location;

/**
 When the gesture paning
 */
- (void)gestureChangedPan:(ZFPlayerGestureControl *)gestureControl
             panDirection:(ZFPanDirection)direction
              panLocation:(ZFPanLocation)location
             withVelocity:(CGPoint)velocity;

/**
 When the end panGesture
 */
- (void)gestureEndedPan:(ZFPlayerGestureControl *)gestureControl
           panDirection:(ZFPanDirection)direction
            panLocation:(ZFPanLocation)location;

/**
 When the pinchGesture changed
 */
- (void)gesturePinched:(ZFPlayerGestureControl *)gestureControl
                 scale:(float)scale;

/**
 When the gesture longPress changed
 */
- (void)longPressed:(ZFPlayerGestureControl *)gestureControl state:(ZFLongPressGestureRecognizerState)state;

#pragma mark - scrollview

/**
 When the player will appear in scrollView.
 */
- (void)playerWillAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
 When the player did appear in scrollView.
 */
- (void)playerDidAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
 When the player will disappear in scrollView.
 */
- (void)playerWillDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
 When the player did disappear in scrollView.
 */
- (void)playerDidDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
 When the player appearing in scrollView.
 */
- (void)playerAppearingInScrollView:(ZFPlayerController *)videoPlayer playerApperaPercent:(CGFloat)playerApperaPercent;

/**
 When the player disappearing in scrollView.
 */
- (void)playerDisappearingInScrollView:(ZFPlayerController *)videoPlayer playerDisapperaPercent:(CGFloat)playerDisapperaPercent;

/**
 When the small float view show.
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer floatViewShow:(BOOL)show;

@end

ZFAVPlayerManager

ZFAVPlayerManager封装AVPlayer对业务层提供遵循ZFPlayerMediaPlayback协议回调

@protocol ZFPlayerMediaPlayback <NSObject>

@required
/// The view must inherited `ZFPlayerView`,this view deals with some gesture conflicts.
@property (nonatomic) ZFPlayerView *view;

/// The player volume.
/// Only affects audio volume for the player instance and not for the device.
/// You can change device volume or player volume as needed,change the player volume you can follow the `ZFPlayerMediaPlayback` protocol.
@property (nonatomic) float volume;

/// The player muted.
/// indicates whether or not audio output of the player is muted. Only affects audio muting for the player instance and not for the device.
/// You can change device volume or player muted as needed,change the player muted you can follow the `ZFPlayerMediaPlayback` protocol.
@property (nonatomic, getter=isMuted) BOOL muted;

/// Playback speed,0.5...2
@property (nonatomic) float rate;

/// The player current play time.
@property (nonatomic, readonly) NSTimeInterval currentTime;

/// The player total time.
@property (nonatomic, readonly) NSTimeInterval totalTime;

/// The player buffer time.
@property (nonatomic, readonly) NSTimeInterval bufferTime;

/// The player seek time.
@property (nonatomic) NSTimeInterval seekTime;

/// The player play state,playing or not playing.
@property (nonatomic, readonly) BOOL isPlaying;

/// Determines how the content scales to fit the view. Defaults to ZFPlayerScalingModeNone.
@property (nonatomic) ZFPlayerScalingMode scalingMode;

/**
 @abstract Check whether video preparation is complete.
 @discussion isPreparedToPlay processing logic
 
 * If isPreparedToPlay is true, you can call [ZFPlayerMediaPlayback play] API start playing;
 * If isPreparedToPlay to false, direct call [ZFPlayerMediaPlayback play], in the play the internal automatic call [ZFPlayerMediaPlayback prepareToPlay] API.
 * Returns true if prepared for playback.
 */
@property (nonatomic, readonly) BOOL isPreparedToPlay;

/// The player should auto player, default is YES.
@property (nonatomic) BOOL shouldAutoPlay;

/// The play asset URL.
@property (nonatomic, nullable) NSURL *assetURL;

/// The video size.
@property (nonatomic) CGSize presentationSize;

/// The playback state.
@property (nonatomic, readonly) ZFPlayerPlaybackState playState;

/// The player load state.
@property (nonatomic, readonly) ZFPlayerLoadState loadState;

///------------------------------------
/// If you don't appoint the controlView, you can called the following blocks.
/// If you appoint the controlView, The following block cannot be called outside, only for `ZFPlayerController` calls.
///------------------------------------

/// The block invoked when the player is Prepare to play.
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// The block invoked when the player is Ready to play.
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// The block invoked when the player play progress changed.
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

/// The block invoked when the player play buffer changed.
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

/// The block invoked when the player playback state changed.
@property (nonatomic, copy, nullable) void(^playerPlayStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

/// The block invoked when the player load state changed.
@property (nonatomic, copy, nullable) void(^playerLoadStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

/// The block invoked when the player play failed.
@property (nonatomic, copy, nullable) void(^playerPlayFailed)(id<ZFPlayerMediaPlayback> asset, id error);

/// The block invoked when the player play end.
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

// The block invoked when video size changed.
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

///------------------------------------
/// end
///------------------------------------

/// Prepares the current queue for playback, interrupting any active (non-mixible) audio sessions.
- (void)prepareToPlay;

/// Reload player.
- (void)reloadPlayer;

/// Play playback.
- (void)play;

/// Pauses playback.
- (void)pause;

/// Replay playback.
- (void)replay;

/// Stop playback.
- (void)stop;

/// Use this method to seek to a specified time for the current player and to be notified when the seek operation is complete.
- (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;

@optional

/// Video UIImage at the current time.
- (UIImage *)thumbnailImageAtCurrentTime;

/// Video UIImage at the current time.
- (void)thumbnailImageAtCurrentTime:(void(^)(UIImage *))handler;

@end

协议的调用基本依赖avplayer, avplayerItem, avasset三者的共同作用

@property (nonatomic, strong, readonly) AVURLAsset *asset;
@property (nonatomic, strong, readonly) AVPlayerItem *playerItem;
@property (nonatomic, strong, readonly) AVPlayer *player;

协议中操作关联通过直接操作avplayer,例如play

- (void)play {
    if (!_isPreparedToPlay) {
        [self prepareToPlay];
    } else {
        [self.player play];
        self.player.rate = self.rate;
        self->_isPlaying = YES;
        self.playState = ZFPlayerPlayStatePlaying;
    }
}

协议中播放器渲染状态等回调对avplayerItem的KVO实现,ZFKVOController用于实现KVO的安全加入和移除.

    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kStatus
                              options:NSKeyValueObservingOptionNew
                              context:nil];
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kPlaybackBufferEmpty
                              options:NSKeyValueObservingOptionNew
                              context:nil];
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kPlaybackLikelyToKeepUp
                              options:NSKeyValueObservingOptionNew
                              context:nil];
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kLoadedTimeRanges
                              options:NSKeyValueObservingOptionNew
                              context:nil];
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:kPresentationSize
                              options:NSKeyValueObservingOptionNew
                              context:nil];


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([keyPath isEqualToString:kStatus]) {

AliyunPlayer

阿里云点播不是一个开源的项目,与avplayer不同,他提供的类似avplayer的功能,通过协议AVPDelegate回调业务使用


@protocol AVPDelegate <NSObject>
@optional

/**
 @brief 播放器事件回调
 @param player 播放器player指针
 @param eventType 播放器事件类型
 @see AVPEventType
 */
/****
 @brief Player event callback.
 @param player Player pointer.
 @param eventType Player event type.
 @see AVPEventType
 */
-(void)onPlayerEvent:(AliPlayer*)player eventType:(AVPEventType)eventType;

/**
 @brief 播放器事件回调
 @param player 播放器player指针
 @param eventWithString 播放器事件类型
 @param description 播放器事件说明
 @see AVPEventType
 */
/****
 @brief Player event callback.
 @param player Player pointer.
 @param eventWithString Player event type.
 @param description Player event description.
 @see AVPEventType
 */
-(void)onPlayerEvent:(AliPlayer*)player eventWithString:(AVPEventWithString)eventWithString description:(NSString *)description;

替换方案

根据原理分析,最简单的替换方式就是使用aliyunPlayer替换avplayer,用AVPDelegat回调e替换avplayerItem的对象监听。

  • 操作的替换,使用aliyunplayer替换avplaer
  • 监听的替换
    status -> onEvent
    periodicTimeObserver -> positionUpdate
    具体可参见代码实现,整个替换过程集中在ZFAVPlayerManager,维持原有ZFPlayerMediaPlayback的回调逻辑

有意思的问题处理

使用大量cell加载后会出现aliplayer会出现黑屏但间隔15秒后悔继续播放。而且其他cell也会出现该问题。

2023-09-20 14:39:24.393392+0800 GKDYVideo[14599:2453453] I/AliFrameWork [_31859558] [AVPBase] :API_IN:Start
2023-09-20 14:39:24.393407+0800 GKDYVideo[14599:2454558] I/AliFrameWork [_31859558] [AVPBase] :API_OUT:apsaraPlayerStatusChanged status from [0] to [1]
2023-09-20 14:39:24.393427+0800 GKDYVideo[14599:2454558] I/AliFrameWork [_31859558] [AVPBase] :API_OUT:apsaraPlayerStatusChanged status from [1] to [2]
2023-09-20 14:39:24.393442+0800 GKDYVideo[14599:2453453] I/AliFrameWork [_31859558] [AVPBase] :API_IN:SetSpeed speed is 1.000000
2023-09-20 14:39:24.393502+0800 GKDYVideo[14599:2453453] I/AliFrameWork [_31859558] [analytics] :eventCode:2046, name:SET_SPEED string:speed * 1.000000 ret:0
2023-09-20 14:39:24.393805+0800 GKDYVideo[14599:2453453] I/AliFrameWork [_31859558] [AFAudioSession] :setActive when app acitve is :1
2023-09-20 14:39:27.758928+0800 GKDYVideo[14599:2453589] [tcp] tcp_input [C7.1.1:3] flags=[R] seq=2574056107, ack=0, win=0 state=LAST_ACK rcv_nxt=2574056107, snd_una=1191748049
2023-09-20 14:39:27.760446+0800 GKDYVideo[14599:2453589] [tcp] tcp_input [C7.1.1:3] flags=[R] seq=2574056107, ack=0, win=0 state=CLOSED rcv_nxt=2574056107, snd_una=1191748049
2023-09-20 14:39:31.375724+0800 GKDYVideo[14599:2453583] [tcp] tcp_input [C8.1.1:3] flags=[R] seq=2517920221, ack=0, win=0 state=LAST_ACK rcv_nxt=2517920221, snd_una=921807051
2023-09-20 14:39:31.376048+0800 GKDYVideo[14599:2453583] [tcp] tcp_input [C8.1.1:3] flags=[R] seq=2517920221, ack=0, win=0 state=CLOSED rcv_nxt=2517920221, snd_una=921807051
2023-09-20 14:39:35.036940+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C9.1.1:3] flags=[R] seq=2411918010, ack=0, win=0 state=LAST_ACK rcv_nxt=2411918010, snd_una=613104395
2023-09-20 14:39:35.037386+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C9.1.1:3] flags=[R] seq=2411918010, ack=0, win=0 state=CLOSED rcv_nxt=2411918010, snd_una=613104395
2023-09-20 14:39:37.696239+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C10.1.1:3] flags=[R] seq=4278652954, ack=0, win=0 state=LAST_ACK rcv_nxt=4278652954, snd_una=1979802224
2023-09-20 14:39:37.696657+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C10.1.1:3] flags=[R] seq=4278652954, ack=0, win=0 state=CLOSED rcv_nxt=4278652954, snd_una=1979802224
2023-09-20 14:39:39.950337+0800 GKDYVideo[14599:2453583] [tcp] tcp_input [C11.1.1:3] flags=[R] seq=273800372, ack=0, win=0 state=LAST_ACK rcv_nxt=273800372, snd_una=2589147071
2023-09-20 14:39:39.950831+0800 GKDYVideo[14599:2453583] [tcp] tcp_input [C11.1.1:3] flags=[R] seq=273800372, ack=0, win=0 state=CLOSED rcv_nxt=273800372, snd_una=2589147071
2023-09-20 14:39:43.433483+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C12.1.1:3] flags=[R] seq=171878393, ack=0, win=0 state=LAST_ACK rcv_nxt=171878393, snd_una=3452434598
2023-09-20 14:39:43.435181+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C12.1.1:3] flags=[R] seq=171878393, ack=0, win=0 state=CLOSED rcv_nxt=171878393, snd_una=3452434598
2023-09-20 14:39:46.607691+0800 GKDYVideo[14599:2453583] [tcp] tcp_input [C13.1.1:3] flags=[R] seq=4278952769, ack=0, win=0 state=LAST_ACK rcv_nxt=4278952769, snd_una=3197826921
2023-09-20 14:39:46.608083+0800 GKDYVideo[14599:2453583] [tcp] tcp_input [C13.1.1:3] flags=[R] seq=4278952769, ack=0, win=0 state=CLOSED rcv_nxt=4278952769, snd_una=3197826921
2023-09-20 14:39:49.267421+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C14.1.1:3] flags=[R] seq=506518360, ack=0, win=0 state=LAST_ACK rcv_nxt=506518360, snd_una=1287075895
2023-09-20 14:39:49.269745+0800 GKDYVideo[14599:2453600] [tcp] tcp_input [C14.1.1:3] flags=[R] seq=506518360, ack=0, win=0 state=CLOSED rcv_nxt=506518360, snd_una=1287075895
2023-09-20 14:39:54.398066+0800 GKDYVideo[14599:2454557] Memory in use (in bytes): 55511296
2023-09-20 14:39:54.400844+0800 GKDYVideo[14599:2453611] I/AliFrameWork [_31859558] [CURLConnection2] :==> ~CURLConnection2
2023-09-20 14:39:54.401127+0800 GKDYVideo[14599:2453611] I/AliFrameWork [_31859558] [CURLConnection2] :<== ~CURLConnection2
2023-09-20 14:39:54.640978+0800 GKDYVideo[14599:2454559] E/AliFrameWork [_31859558] [CURLConnection2] :FillBuffer - Failed: Stream error in the HTTP/2 framing layer(92)

关键错误是FillBuffer - Failed: Stream error in the HTTP/2 framing layer(92),按照我的猜测是短时间生成多个aliplayer后,所有网络数据接收缓存的streamsocket都会被看门狗中断的情况,需要15秒错误检测后才能继续播放,关键这15秒不仅发生在当前aliplayer,而是所有aliplayer重新start的时候都需要等待这15秒去检测错误。于是为了解决这个问题使用一个workaround的方案,重设错误检测时间,将15秒降为1秒

    AVPConfig *config = [self.player getConfig];
    config.networkTimeout = 1000;
    [self.player setConfig:config];

这是一个有意思的错误,而且多次并发后会造成全部buffer接受过程受到影响。最合理的解决方案,应该是业务减少aliplayer的并发数量以避免出现这种问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值