背景简介
一个使用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的并发数量以避免出现这种问题