提交 8ea0a67a authored 作者: kongdywang's avatar kongdywang

1. Added Picture-in-Picture capability for iOS live streaming.

2. Fixed the Picture-in-Picture service leak issue on Android.
上级 b4fe752b
...@@ -6,6 +6,7 @@ import android.graphics.Bitmap; ...@@ -6,6 +6,7 @@ import android.graphics.Bitmap;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.os.Bundle; import android.os.Bundle;
import android.view.Surface; import android.view.Surface;
import android.view.TextureView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
...@@ -65,7 +66,7 @@ public class FTXLivePlayer extends FTXBasePlayer implements TXFlutterLivePlayerA ...@@ -65,7 +66,7 @@ public class FTXLivePlayer extends FTXBasePlayer implements TXFlutterLivePlayerA
private final FTXV2LiveObserver mObserver; private final FTXV2LiveObserver mObserver;
private int mLastPlayEvent = -1; private int mLastPlayEvent = -1;
private boolean mIsPaused = false; private boolean mIsPaused = false;
private FtxMessages.TXLivePlayerFlutterAPI mLiveFlutterApi; private final FtxMessages.TXLivePlayerFlutterAPI mLiveFlutterApi;
private final FTXPIPManager.PipCallback pipCallback = new FTXPIPManager.PipCallback() { private final FTXPIPManager.PipCallback pipCallback = new FTXPIPManager.PipCallback() {
@Override @Override
......
...@@ -204,6 +204,7 @@ public class SuperPlayerPlugin implements FlutterPlugin, ActivityAware, ...@@ -204,6 +204,7 @@ public class SuperPlayerPlugin implements FlutterPlugin, ActivityAware,
mPlayers.append(playerId, player); mPlayers.append(playerId, player);
PlayerMsg playerMsg = new PlayerMsg(); PlayerMsg playerMsg = new PlayerMsg();
playerMsg.setPlayerId((long) playerId); playerMsg.setPlayerId((long) playerId);
LiteavLog.i(TAG, "createVodPlayer :" + playerId);
return playerMsg; return playerMsg;
} }
...@@ -215,6 +216,7 @@ public class SuperPlayerPlugin implements FlutterPlugin, ActivityAware, ...@@ -215,6 +216,7 @@ public class SuperPlayerPlugin implements FlutterPlugin, ActivityAware,
mPlayers.append(playerId, player); mPlayers.append(playerId, player);
PlayerMsg playerMsg = new PlayerMsg(); PlayerMsg playerMsg = new PlayerMsg();
playerMsg.setPlayerId((long) playerId); playerMsg.setPlayerId((long) playerId);
LiteavLog.i(TAG, "createLivePlayer :" + playerId);
return playerMsg; return playerMsg;
} }
...@@ -229,8 +231,10 @@ public class SuperPlayerPlugin implements FlutterPlugin, ActivityAware, ...@@ -229,8 +231,10 @@ public class SuperPlayerPlugin implements FlutterPlugin, ActivityAware,
public void releasePlayer(@NonNull PlayerMsg playerId) { public void releasePlayer(@NonNull PlayerMsg playerId) {
if (null != playerId.getPlayerId()) { if (null != playerId.getPlayerId()) {
int intPlayerId = playerId.getPlayerId().intValue(); int intPlayerId = playerId.getPlayerId().intValue();
LiteavLog.i(TAG, "releasePlayer :" + intPlayerId);
FTXBasePlayer player = mPlayers.get(intPlayerId); FTXBasePlayer player = mPlayers.get(intPlayerId);
if (player != null) { if (player != null) {
LiteavLog.i(TAG, "releasePlayer start destroy player :" + intPlayerId);
player.destroy(); player.destroy();
mPlayers.remove(intPlayerId); mPlayers.remove(intPlayerId);
} }
......
...@@ -498,7 +498,7 @@ public class FlutterPipImplActivity extends Activity implements TextureView.Surf ...@@ -498,7 +498,7 @@ public class FlutterPipImplActivity extends Activity implements TextureView.Surf
private void bindAndroid12BugServiceIfNeed() { private void bindAndroid12BugServiceIfNeed() {
if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
Intent serviceIntent = new Intent(this, TXAndroid12BridgeService.class); Intent serviceIntent = new Intent(getApplicationContext(), TXAndroid12BridgeService.class);
startService(serviceIntent); startService(serviceIntent);
bindService(serviceIntent, this, Context.BIND_AUTO_CREATE); bindService(serviceIntent, this, Context.BIND_AUTO_CREATE);
} }
......
...@@ -45,6 +45,12 @@ ...@@ -45,6 +45,12 @@
/// PIP function is not started. /// PIP function is not started.
/// PIP功能没有启动 /// PIP功能没有启动
#define ERROR_IOS_PIP_NOT_RUNNING -111 #define ERROR_IOS_PIP_NOT_RUNNING -111
/// PIP start time out
/// PIP 启动超时
#define ERROR_IOS_PIP_START_TIME_OUT -112
/// Insufficient permissions, currently only appears in Picture-in-Picture live streaming
/// 权限不足,目前只出现在直播画中画
#define ERROR_PIP_AUTH_DENIED -201
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "FTXBasePlayer.h" #import "FTXBasePlayer.h"
#import "FTXVodPlayerDelegate.h"
@protocol FlutterPluginRegistrar; @protocol FlutterPluginRegistrar;
...@@ -11,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -11,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface FTXLivePlayer : FTXBasePlayer @interface FTXLivePlayer : FTXBasePlayer
@property(nonatomic, weak) id<FTXVodPlayerDelegate> delegate;
- (instancetype)initWithRegistrar:(id<FlutterPluginRegistrar>)registrar; - (instancetype)initWithRegistrar:(id<FlutterPluginRegistrar>)registrar;
- (void)notifyAppTerminate:(UIApplication *)application; - (void)notifyAppTerminate:(UIApplication *)application;
......
...@@ -11,10 +11,14 @@ ...@@ -11,10 +11,14 @@
#import "FTXLog.h" #import "FTXLog.h"
#import <stdatomic.h> #import <stdatomic.h>
#import "FTXV2LiveTools.h" #import "FTXV2LiveTools.h"
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "FTXPipController.h"
#import "FTXImgTools.h"
static const int uninitialized = -1; static const int uninitialized = -1;
@interface FTXLivePlayer ()<FlutterTexture, V2TXLivePlayerObserver, TXFlutterLivePlayerApi> @interface FTXLivePlayer ()<FlutterTexture, V2TXLivePlayerObserver, TXFlutterLivePlayerApi, FTXLivePipDelegate, FTXPipPlayerDelegate>
@property (nonatomic, strong) V2TXLivePlayer *livePlayer; @property (nonatomic, strong) V2TXLivePlayer *livePlayer;
@property (nonatomic, assign) int lastPlayEvent; @property (nonatomic, assign) int lastPlayEvent;
...@@ -22,6 +26,10 @@ static const int uninitialized = -1; ...@@ -22,6 +26,10 @@ static const int uninitialized = -1;
@property (nonatomic, assign) BOOL isOpenedPip; @property (nonatomic, assign) BOOL isOpenedPip;
@property (nonatomic, assign) BOOL isPaused; @property (nonatomic, assign) BOOL isPaused;
@property (nonatomic, strong) TXLivePlayerFlutterAPI* liveFlutterApi; @property (nonatomic, strong) TXLivePlayerFlutterAPI* liveFlutterApi;
@property (nonatomic, assign) BOOL hasEnteredPipMode;
@property (nonatomic, assign) BOOL isStartEnterPipMode;
@property (nonatomic, assign) BOOL restoreUI;
@property (nonatomic, assign) CGSize liveSize;
@end @end
...@@ -49,6 +57,10 @@ static const int uninitialized = -1; ...@@ -49,6 +57,10 @@ static const int uninitialized = -1;
_textureId = -1; _textureId = -1;
self.isOpenedPip = NO; self.isOpenedPip = NO;
self.lastPlayEvent = -1; self.lastPlayEvent = -1;
self.hasEnteredPipMode = NO;
self.isStartEnterPipMode = NO;
self.restoreUI = NO;
self.liveSize = CGSizeZero;
SetUpTXFlutterLivePlayerApiWithSuffix([registrar messenger], self, [self.playerId stringValue]); SetUpTXFlutterLivePlayerApiWithSuffix([registrar messenger], self, [self.playerId stringValue]);
self.liveFlutterApi = [[TXLivePlayerFlutterAPI alloc] initWithBinaryMessenger:[registrar messenger] messageChannelSuffix:[self.playerId stringValue]]; self.liveFlutterApi = [[TXLivePlayerFlutterAPI alloc] initWithBinaryMessenger:[registrar messenger] messageChannelSuffix:[self.playerId stringValue]];
} }
...@@ -122,11 +134,41 @@ static const int uninitialized = -1; ...@@ -122,11 +134,41 @@ static const int uninitialized = -1;
} }
if (nil != self.livePlayer) { if (nil != self.livePlayer) {
[self.livePlayer enableObserveVideoFrame:YES pixelFormat:V2TXLivePixelFormatBGRA32 bufferType:V2TXLiveBufferTypePixelBuffer]; [self.livePlayer enableObserveVideoFrame:YES pixelFormat:V2TXLivePixelFormatBGRA32 bufferType:V2TXLiveBufferTypePixelBuffer];
[self.livePlayer setProperty:@"enableBackgroundDecoding" value:@(YES)];
} }
} }
} }
#pragma mark - #pragma mark - private method
/**
Check if the current language is Simplified Chinese.
*/
- (BOOL)isCurrentLanguageHans
{
NSArray *languages = [NSLocale preferredLanguages];
NSString *currentLanguage = [languages objectAtIndex:0];
if ([currentLanguage isEqualToString:@"zh-Hans-CN"])
{
return YES;
}
return NO;
}
- (CVPixelBufferRef)getPipImagePixelBuffer
{
NSString *imagePath;
if ([self isCurrentLanguageHans]) {
imagePath = [[NSBundle mainBundle] pathForResource:@"pictureInpicture_zh" ofType:@"jpg"];
} else {
imagePath = [[NSBundle mainBundle] pathForResource:@"pictureInpicture_en" ofType:@"jpg"];
}
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
// must create new obj when evey called
return [FTXImgTools CVPixelBufferRefFromUiImage:image];
}
- (NSNumber*)createPlayer:(BOOL)onlyAudio - (NSNumber*)createPlayer:(BOOL)onlyAudio
{ {
...@@ -141,7 +183,7 @@ static const int uninitialized = -1; ...@@ -141,7 +183,7 @@ static const int uninitialized = -1;
- (UIView *)txPipView { - (UIView *)txPipView {
if (!_txPipView) { if (!_txPipView) {
// Set the size to 1 pixel to ensure proper display in PIP. // Set the size to 1 pixel to ensure proper display in PIP.
_txPipView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)]; _txPipView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
} }
return _txPipView; return _txPipView;
} }
...@@ -178,7 +220,6 @@ static const int uninitialized = -1; ...@@ -178,7 +220,6 @@ static const int uninitialized = -1;
- (void)setRenderRotation:(int)rotation - (void)setRenderRotation:(int)rotation
{ {
if (self.livePlayer != nil) { if (self.livePlayer != nil) {
[self.livePlayer setRenderRotation:[FTXV2LiveTools transRotationFromDegree:rotation]]; [self.livePlayer setRenderRotation:[FTXV2LiveTools transRotationFromDegree:rotation]];
} }
...@@ -200,6 +241,7 @@ static const int uninitialized = -1; ...@@ -200,6 +241,7 @@ static const int uninitialized = -1;
[self.livePlayer resumeVideo]; [self.livePlayer resumeVideo];
self.lastPlayEvent = -1; self.lastPlayEvent = -1;
self.isPaused = NO; self.isPaused = NO;
self.liveSize = CGSizeZero;
return (int)[self.livePlayer startLivePlay:url]; return (int)[self.livePlayer startLivePlay:url];
} }
return uninitialized; return uninitialized;
...@@ -211,6 +253,10 @@ static const int uninitialized = -1; ...@@ -211,6 +253,10 @@ static const int uninitialized = -1;
_isStoped = YES; _isStoped = YES;
self.lastPlayEvent = -1; self.lastPlayEvent = -1;
self.isPaused = NO; self.isPaused = NO;
self.liveSize = CGSizeZero;
if (self.hasEnteredPipMode) {
[[FTXPipController shareInstance] exitPip];
}
return [self.livePlayer stopPlay]; return [self.livePlayer stopPlay];
} }
return NO; return NO;
...@@ -226,6 +272,14 @@ static const int uninitialized = -1; ...@@ -226,6 +272,14 @@ static const int uninitialized = -1;
- (void)pause - (void)pause
{ {
if (self.hasEnteredPipMode) {
[[FTXPipController shareInstance] pausePipVideo];
} else {
[self pauseImpl];
}
}
- (void)pauseImpl {
if (self.livePlayer != nil) { if (self.livePlayer != nil) {
[self.livePlayer pauseVideo]; [self.livePlayer pauseVideo];
[self.livePlayer pauseAudio]; [self.livePlayer pauseAudio];
...@@ -235,6 +289,14 @@ static const int uninitialized = -1; ...@@ -235,6 +289,14 @@ static const int uninitialized = -1;
- (void)resume - (void)resume
{ {
if (self.hasEnteredPipMode) {
[[FTXPipController shareInstance] resumePipVideo];
} else {
[self resumeImpl];
}
}
- (void)resumeImpl {
if (self.livePlayer != nil) { if (self.livePlayer != nil) {
[self.livePlayer resumeVideo]; [self.livePlayer resumeVideo];
[self.livePlayer resumeAudio]; [self.livePlayer resumeAudio];
...@@ -338,6 +400,25 @@ static const int uninitialized = -1; ...@@ -338,6 +400,25 @@ static const int uninitialized = -1;
FTXLOGI(@"onLivePlayEvent:%i,%@", evtID, params[EVT_MSG]) FTXLOGI(@"onLivePlayEvent:%i,%@", evtID, params[EVT_MSG])
} }
- (NSNumber*)enablePictureInPicture:(BOOL)isEnabled {
if (self.livePlayer) {
if (isEnabled != self.isOpenedPip) {
if (isEnabled) {
UIViewController* flutterVC = [self getFlutterViewController];
[flutterVC.view addSubview:self.txPipView];
[self.livePlayer setRenderView:self.txPipView];
} else if (nil != self->_txPipView) {
[self->_txPipView removeFromSuperview];
self->_txPipView = nil;
}
self.isOpenedPip = isEnabled;
int result = (int)[self.livePlayer enablePictureInPicture:isEnabled];
return @(result);
}
}
return @(uninitialized);
}
#pragma mark - FlutterTexture #pragma mark - FlutterTexture
- (CVPixelBufferRef _Nullable)copyPixelBuffer - (CVPixelBufferRef _Nullable)copyPixelBuffer
...@@ -345,6 +426,9 @@ static const int uninitialized = -1; ...@@ -345,6 +426,9 @@ static const int uninitialized = -1;
if(_isTerminate || _isStoped){ if(_isTerminate || _isStoped){
return nil; return nil;
} }
if (self.hasEnteredPipMode) {
return [self getPipImagePixelBuffer];
}
CVPixelBufferRef pixelBuffer = _latestPixelBuffer; CVPixelBufferRef pixelBuffer = _latestPixelBuffer;
while (!atomic_compare_exchange_strong_explicit(&_latestPixelBuffer, &pixelBuffer, NULL, memory_order_release, memory_order_relaxed)) { while (!atomic_compare_exchange_strong_explicit(&_latestPixelBuffer, &pixelBuffer, NULL, memory_order_release, memory_order_relaxed)) {
pixelBuffer = _latestPixelBuffer; pixelBuffer = _latestPixelBuffer;
...@@ -360,12 +444,23 @@ static const int uninitialized = -1; ...@@ -360,12 +444,23 @@ static const int uninitialized = -1;
} }
- (nullable IntMsg *)enterPictureInPictureModePipParamsMsg:(nonnull PipParamsPlayerMsg *)pipParamsMsg error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (nullable IntMsg *)enterPictureInPictureModePipParamsMsg:(nonnull PipParamsPlayerMsg *)pipParamsMsg error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
// [self.livePlayer enablePictureInPicture:YES]; if (self.liveSize.width <= 0 || self.liveSize.height <= 0) {
return [TXCommonUtil intMsgWith:@(NO_ERROR)];; FTXLOGE(@"live pip opened failed, size is invalid");
return [TXCommonUtil intMsgWith:@(ERROR_IOS_PIP_PLAYER_NOT_EXIST)];
}
int retCode = [[FTXPipController shareInstance] startOpenPip:self.livePlayer withSize:self.liveSize];
if (retCode == NO_ERROR) {
[FTXPipController shareInstance].pipDelegate = self;
[FTXPipController shareInstance].playerDelegate = self;
self.isStartEnterPipMode = YES;
}
return [TXCommonUtil intMsgWith:@(retCode)];
} }
- (void)exitPictureInPictureModePlayerMsg:(nonnull PlayerMsg *)playerMsg error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (void)exitPictureInPictureModePlayerMsg:(nonnull PlayerMsg *)playerMsg error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
//FlutterMethodNotImplemented if (self.hasEnteredPipMode) {
[[FTXPipController shareInstance] exitPip];
}
} }
- (nullable IntMsg *)initializeOnlyAudio:(nonnull BoolPlayerMsg *)onlyAudio error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (nullable IntMsg *)initializeOnlyAudio:(nonnull BoolPlayerMsg *)onlyAudio error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
...@@ -418,27 +513,10 @@ static const int uninitialized = -1; ...@@ -418,27 +513,10 @@ static const int uninitialized = -1;
return [TXCommonUtil intMsgWith:@(r)]; return [TXCommonUtil intMsgWith:@(r)];
} }
- (nullable NSNumber *)enablePictureInPictureMsg:(nonnull BoolPlayerMsg *)msg error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (nullable NSNumber *)enablePictureInPictureMsg:(nonnull BoolPlayerMsg *)msg error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
if (self.livePlayer) { return [self enablePictureInPicture:[msg.value boolValue]];
BOOL dstFlag = [msg.value boolValue];
if (dstFlag != self.isOpenedPip) {
if ([msg.value boolValue]) {
UIViewController* flutterVC = [self getFlutterViewController];
[flutterVC.view addSubview:self.txPipView];
[self.livePlayer setRenderView:self.txPipView];
} else if (nil != self->_txPipView) {
[self->_txPipView removeFromSuperview];
self->_txPipView = nil;
}
self.isOpenedPip = dstFlag;
int result = (int)[self.livePlayer enablePictureInPicture:[msg.value boolValue]];
return @(result);
}
}
return @(uninitialized);
} }
- (nullable NSNumber *)enableReceiveSeiMessagePlayerMsg:(nonnull PlayerMsg *)playerMsg isEnabled:(nonnull NSNumber *)isEnabled payloadType:(nonnull NSNumber *)payloadType error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (nullable NSNumber *)enableReceiveSeiMessagePlayerMsg:(nonnull PlayerMsg *)playerMsg isEnabled:(nonnull NSNumber *)isEnabled payloadType:(nonnull NSNumber *)payloadType error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
if (self.livePlayer) { if (self.livePlayer) {
int result = (int)[self.livePlayer enableReceiveSeiMessage:[isEnabled boolValue] payloadType:[payloadType intValue]]; int result = (int)[self.livePlayer enableReceiveSeiMessage:[isEnabled boolValue] payloadType:[payloadType intValue]];
...@@ -532,6 +610,7 @@ static const int uninitialized = -1; ...@@ -532,6 +610,7 @@ static const int uninitialized = -1;
EVT_PARAM2 : @(height), EVT_PARAM2 : @(height),
EVT_MSG : [NSString stringWithFormat:@"Resolution changed. resolution:%ldx%ld", (long)width, (long)height] EVT_MSG : [NSString stringWithFormat:@"Resolution changed. resolution:%ldx%ld", (long)width, (long)height]
}; };
self.liveSize = CGSizeMake(width, height);
[self notifyPlayerEvent:evtID withParams:param]; [self notifyPlayerEvent:evtID withParams:param];
} }
...@@ -684,13 +763,16 @@ static const int uninitialized = -1; ...@@ -684,13 +763,16 @@ static const int uninitialized = -1;
} }
old = _latestPixelBuffer; old = _latestPixelBuffer;
} }
if (old && old != pixelBuffer) { if (old && old != pixelBuffer) {
CFRelease(old); CFRelease(old);
} }
if (_textureId >= 0 && _textureRegistry) { if (_textureId >= 0 && _textureRegistry) {
[_textureRegistry textureFrameAvailable:_textureId]; [_textureRegistry textureFrameAvailable:_textureId];
} }
if (self.isStartEnterPipMode) {
CVPixelBufferRef pipPixelBuffer = _latestPixelBuffer;
[[FTXPipController shareInstance] displayPixelBuffer:pipPixelBuffer];
}
} }
} }
...@@ -800,7 +882,103 @@ static const int uninitialized = -1; ...@@ -800,7 +882,103 @@ static const int uninitialized = -1;
* @param storagePath 录制的文件地址。 * @param storagePath 录制的文件地址。
*/ */
- (void)onLocalRecordComplete:(id<V2TXLivePlayer>)player errCode:(NSInteger)errCode storagePath:(NSString *)storagePath { - (void)onLocalRecordComplete:(id<V2TXLivePlayer>)player errCode:(NSInteger)errCode storagePath:(NSString *)storagePath {
}
#pragma mark - FTXLivePipDelegate
- (void)pictureInPictureErrorDidOccur:(FTX_LIVE_PIP_ERROR)errorStatus {
NSInteger type = errorStatus;
switch (errorStatus) {
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_NONE:
type = NO_ERROR;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_DEVICE_NOT_SUPPORT:
type = ERROR_IOS_PIP_DEVICE_NOT_SUPPORT;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_PLAYER_NOT_SUPPORT:
type = ERROR_IOS_PIP_PLAYER_NOT_SUPPORT;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_VIDEO_NOT_SUPPORT:
type = ERROR_IOS_PIP_VIDEO_NOT_SUPPORT;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_NOT_POSSIBLE:
type = ERROR_IOS_PIP_IS_NOT_POSSIBLE;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_ERROR_FROM_SYSTEM:
type = ERROR_IOS_PIP_FROM_SYSTEM;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_PLAYER_NOT_EXIST:
type = ERROR_IOS_PIP_PLAYER_NOT_EXIST;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_RUNNING:
type = ERROR_IOS_PIP_IS_RUNNING;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_NOT_RUNNING:
type = ERROR_IOS_PIP_NOT_RUNNING;
break;
case FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_START_TIMEOUT:
type = ERROR_IOS_PIP_START_TIME_OUT;
break;
default:
type = errorStatus;
break;
}
self.hasEnteredPipMode = NO;
self.isStartEnterPipMode = NO;
FTXLOGE(@"[onPlayer], pictureInPictureErrorDidOccur errorType= %ld", type);
if (self.delegate && [self.delegate respondsToSelector:@selector(onPlayerPipStateError:)]) {
[self.delegate onPlayerPipStateError:type];
}
}
- (void)pictureInPictureStateDidChange:(TX_VOD_PLAYER_PIP_STATE)pipState {
if (pipState == TX_VOD_PLAYER_PIP_STATE_DID_START) {
self.hasEnteredPipMode = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(onPlayerPipStateDidStart)]) {
[self.delegate onPlayerPipStateDidStart];
}
}
if (pipState == TX_VOD_PLAYER_PIP_STATE_WILL_STOP) {
self.isStartEnterPipMode = NO;
if (self.delegate && [self.delegate respondsToSelector:@selector(onPlayerPipStateWillStop)]) {
[self.delegate onPlayerPipStateWillStop];
}
}
if (pipState == TX_VOD_PLAYER_PIP_STATE_DID_STOP) {
self.hasEnteredPipMode = NO;
[[FTXPipController shareInstance] exitPip];
if (self.restoreUI) {
self.restoreUI = NO;
} else {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(onPlayerPipStateDidStop)]) {
[self.delegate onPlayerPipStateDidStop];
}
});
}
}
if (pipState == TX_VOD_PLAYER_PIP_STATE_RESTORE_UI) {
self.restoreUI = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[self resume];
});
if (self.delegate && [self.delegate respondsToSelector:@selector(onPlayerPipStateRestoreUI:)]) {
[self.delegate onPlayerPipStateRestoreUI:0];
}
}
self.isStartEnterPipMode = self.hasEnteredPipMode;
}
- (void)playerStateDidChange:(FTXAVPlayerState)playerState {
if (playerState == FTXAVPlayerStatePlaying) {
[self resumeImpl];
}
// else if (playerState == FTXAVPlayerStatePaused) {
// [self pauseImpl];
// }
} }
@end @end
...@@ -4,29 +4,12 @@ ...@@ -4,29 +4,12 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "FTXBasePlayer.h" #import "FTXBasePlayer.h"
#import "FTXVodPlayerDelegate.h"
@protocol FlutterPluginRegistrar; @protocol FlutterPluginRegistrar;
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@protocol FTXVodPlayerDelegate <NSObject>
- (void)onPlayerPipRequestStart;
- (void)onPlayerPipStateDidStart;
- (void)onPlayerPipStateWillStop;
- (void)onPlayerPipStateDidStop;
- (void)onPlayerPipStateRestoreUI:(double)playTime;;
- (void)onPlayerPipStateError:(NSInteger)errorId;
- (void) releasePlayerInner:(NSNumber*)playerId;
@end
@interface FTXVodPlayer : FTXBasePlayer @interface FTXVodPlayer : FTXBasePlayer
@property(nonatomic, weak) id<FTXVodPlayerDelegate> delegate; @property(nonatomic, weak) id<FTXVodPlayerDelegate> delegate;
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
#import "TXCommonUtil.h" #import "TXCommonUtil.h"
#import "FTXLog.h" #import "FTXLog.h"
#import <stdatomic.h> #import <stdatomic.h>
#import "FTXImgTools.h"
static const int uninitialized = -1; static const int uninitialized = -1;
static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003; static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003;
...@@ -547,7 +548,8 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003; ...@@ -547,7 +548,8 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003;
} }
UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
return [self CVPixelBufferRefFromUiImage:image]; // must create new obj when evey called
return [FTXImgTools CVPixelBufferRefFromUiImage:image];
} }
- (void)setPlayerConfig:(FTXVodPlayConfigPlayerMsg *)args - (void)setPlayerConfig:(FTXVodPlayConfigPlayerMsg *)args
...@@ -792,6 +794,12 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003; ...@@ -792,6 +794,12 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003;
case TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_NOT_RUNNING: case TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_NOT_RUNNING:
type = ERROR_IOS_PIP_NOT_RUNNING; type = ERROR_IOS_PIP_NOT_RUNNING;
break; break;
case TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_START_TIMEOUT:
type = ERROR_IOS_PIP_START_TIME_OUT;
break;
default:
type = errorType;
break;
} }
self.hasEnteredPipMode = NO; self.hasEnteredPipMode = NO;
FTXLOGE(@"[onPlayer], pictureInPictureErrorDidOccur errorType= %ld", type); FTXLOGE(@"[onPlayer], pictureInPictureErrorDidOccur errorType= %ld", type);
...@@ -807,71 +815,6 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003; ...@@ -807,71 +815,6 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003;
- (void)onPlayer:(TXVodPlayer *)player airPlayStateDidChange:(TX_VOD_PLAYER_AIRPLAY_STATE)airPlayState withParam:(NSDictionary *)param { - (void)onPlayer:(TXVodPlayer *)player airPlayStateDidChange:(TX_VOD_PLAYER_AIRPLAY_STATE)airPlayState withParam:(NSDictionary *)param {
} }
#pragma mark - Convert UIImage to CVPixelBufferRef
- (CVPixelBufferRef)CVPixelBufferRefFromUiImage:(UIImage *)img {
CGSize size = img.size;
CGImageRef image = [img CGImage];
BOOL hasAlpha = CGImageRefContainsAlpha(image);
CFDictionaryRef empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
empty, kCVPixelBufferIOSurfacePropertiesKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, inputPixelFormat(), (__bridge CFDictionaryRef) options, &pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
uint32_t bitmapInfo = bitmapInfoWithPixelFormatType(inputPixelFormat(), (bool)hasAlpha);
CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, CVPixelBufferGetBytesPerRow(pxbuffer), rgbColorSpace, bitmapInfo);
NSParameterAssert(context);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
return pxbuffer;
}
static OSType inputPixelFormat(){
return kCVPixelFormatType_32BGRA;
}
static uint32_t bitmapInfoWithPixelFormatType(OSType inputPixelFormat, bool hasAlpha){
if (inputPixelFormat == kCVPixelFormatType_32BGRA) {
uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
if (!hasAlpha) {
bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host;
}
return bitmapInfo;
}else if (inputPixelFormat == kCVPixelFormatType_32ARGB) {
uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big;
return bitmapInfo;
}else{
return 0;
}
}
// Check alpha value
BOOL CGImageRefContainsAlpha(CGImageRef imageRef) {
if (!imageRef) {
return NO;
}
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}
#pragma mark - TXFlutterVodPlayerApi #pragma mark - TXFlutterVodPlayerApi
- (nullable BoolMsg *)enableHardwareDecodeEnable:(nonnull BoolPlayerMsg *)enable error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (nullable BoolMsg *)enableHardwareDecodeEnable:(nonnull BoolPlayerMsg *)enable error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
......
...@@ -116,6 +116,7 @@ SuperPlayerPlugin* instance; ...@@ -116,6 +116,7 @@ SuperPlayerPlugin* instance;
FTXLOGV(@"called releasePlayerInner,%@ is start release", playerId); FTXLOGV(@"called releasePlayerInner,%@ is start release", playerId);
FTXBasePlayer *player = [_players objectForKey:playerId]; FTXBasePlayer *player = [_players objectForKey:playerId];
if (player != nil) { if (player != nil) {
FTXLOGI(@"releasePlayer start destroy player :%@", playerId);
[player destory]; [player destory];
[_players removeObjectForKey:playerId]; [_players removeObjectForKey:playerId];
} }
...@@ -234,8 +235,10 @@ SuperPlayerPlugin* instance; ...@@ -234,8 +235,10 @@ SuperPlayerPlugin* instance;
- (nullable PlayerMsg *)createLivePlayerWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { - (nullable PlayerMsg *)createLivePlayerWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error {
FTXLivePlayer* player = [[FTXLivePlayer alloc] initWithRegistrar:self.registrar]; FTXLivePlayer* player = [[FTXLivePlayer alloc] initWithRegistrar:self.registrar];
player.delegate = self;
NSNumber *playerId = player.playerId; NSNumber *playerId = player.playerId;
_players[playerId] = player; _players[playerId] = player;
FTXLOGI(@"createLivePlayer :%@", playerId);
return [TXCommonUtil playerMsgWith:playerId]; return [TXCommonUtil playerMsgWith:playerId];
} }
...@@ -244,6 +247,7 @@ SuperPlayerPlugin* instance; ...@@ -244,6 +247,7 @@ SuperPlayerPlugin* instance;
player.delegate = self; player.delegate = self;
NSNumber *playerId = player.playerId; NSNumber *playerId = player.playerId;
_players[playerId] = player; _players[playerId] = player;
FTXLOGI(@"createVodPlayer :%@", playerId);
return [TXCommonUtil playerMsgWith:playerId]; return [TXCommonUtil playerMsgWith:playerId];
} }
......
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_COMMON_FTXPLAYERCONSTANTS_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_COMMON_FTXPLAYERCONSTANTS_H_
#import "FTXLiteAVSDKHeader.h"
// 点播高级套餐 Feature
#define TUI_FEATURE_PLAYER_PREMIUM (0b10000000)
// 点播企业套餐 Feature
#define TUI_FEATURE_PLAYER_ENTERPRISE (0b100000000)
// pip status
typedef NS_ENUM(NSInteger, FTX_LIVE_PIP_ERROR) {
/// 无错误
FTX_VOD_PLAYER_PIP_ERROR_TYPE_NONE = TX_VOD_PLAYER_PIP_ERROR_TYPE_NONE,
/// 设备或系统版本不支持(iPad iOS9+ 才支持PIP)
FTX_VOD_PLAYER_PIP_ERROR_TYPE_DEVICE_NOT_SUPPORT = TX_VOD_PLAYER_PIP_ERROR_TYPE_DEVICE_NOT_SUPPORT,
/// 播放器不支持
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PLAYER_NOT_SUPPORT = TX_VOD_PLAYER_PIP_ERROR_TYPE_PLAYER_NOT_SUPPORT,
/// 视频不支持
FTX_VOD_PLAYER_PIP_ERROR_TYPE_VIDEO_NOT_SUPPORT = TX_VOD_PLAYER_PIP_ERROR_TYPE_VIDEO_NOT_SUPPORT,
/// PIP控制器不可用
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_NOT_POSSIBLE = TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_NOT_POSSIBLE,
/// PIP控制器报错
FTX_VOD_PLAYER_PIP_ERROR_TYPE_ERROR_FROM_SYSTEM = TX_VOD_PLAYER_PIP_ERROR_TYPE_ERROR_FROM_SYSTEM,
/// 播放器对象不存在
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PLAYER_NOT_EXIST = TX_VOD_PLAYER_PIP_ERROR_TYPE_PLAYER_NOT_EXIST,
/// PIP功能已经运行
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_RUNNING = TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_RUNNING,
/// PIP功能没有启动
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_NOT_RUNNING = TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_NOT_RUNNING,
/// PIP启动超时
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_START_TIMEOUT = TX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_START_TIMEOUT,
/// pip 没有 sdk 权限
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_AUTH_DENIED = 101,
// 缺乏画中画 bundle 资源
FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_MISS_RESOURCE = 102,
};
/**
后台画中画播放器的播放状态
*/
typedef NS_ENUM(NSInteger, FTXAVPlayerState) {
FTXAVPlayerStateIdle = 0, // 初始状态
FTXAVPlayerStatePrepared, // 播放准备完毕
FTXAVPlayerStatePlaying, // 播放中
FTXAVPlayerStatePaused, // 播放暂停
FTXAVPlayerStateStopped, // 播放停止
FTXAVPlayerStateComplete, // 播放完毕
FTXAVPlayerStateError, // 播放失败
};
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_COMMON_FTXPLAYERCONSTANTS_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_FTXLIVEPIPDELEGATE_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_FTXLIVEPIPDELEGATE_H_
#import "FTXLiteAVSDKHeader.h"
#import "FTXPlayerConstants.h"
@protocol FTXLivePipDelegate <NSObject>
- (void)pictureInPictureStateDidChange:(TX_VOD_PLAYER_PIP_STATE)status;
- (void)pictureInPictureErrorDidOccur:(FTX_LIVE_PIP_ERROR)errorStatus;
@end
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_FTXLIVEPIPDELEGATE_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_FTXVODPLAYERDELEGATE_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_FTXVODPLAYERDELEGATE_H_
@protocol FTXVodPlayerDelegate <NSObject>
- (void)onPlayerPipRequestStart;
- (void)onPlayerPipStateDidStart;
- (void)onPlayerPipStateWillStop;
- (void)onPlayerPipStateDidStop;
- (void)onPlayerPipStateRestoreUI:(double)playTime;;
- (void)onPlayerPipStateError:(NSInteger)errorId;
- (void) releasePlayerInner:(NSNumber*)playerId;
@end
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_FTXVODPLAYERDELEGATE_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_TXPIPAUTH_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_TXPIPAUTH_H_
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TXPipAuth : NSObject
+ (instancetype)shareInstance;
+ (BOOL)cpa;
@end
NS_ASSUME_NONNULL_END
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_HELPER_TXPIPAUTH_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import "TXPipAuth.h"
#import "FTXPlayerConstants.h"
#define TUI_RGSKEY_PARAM1 @"KEY_PARAM1"
#define TUI_RETKEY_PARAM1 @"KEY_PARAM1"
#define TUI_ID_CHECK_FEATURE_AUTH (2) ///< 校验某个 feature 是否授权
@implementation TXPipAuth
+ (instancetype)shareInstance {
static TXPipAuth *g_playerAuth = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
g_playerAuth = [[self alloc] init];
});
return g_playerAuth;
}
+ (BOOL)cpa{
return [TXPipAuth cfa:TUI_FEATURE_PLAYER_PREMIUM];
}
+ (BOOL)cfa:(int)featureId {
// 输入参数
NSMutableDictionary *inputParams = [NSMutableDictionary dictionary];
NSString *featureIdStr = [NSString stringWithFormat:@"%@", @(featureId)];
[inputParams setObject:featureIdStr forKey:TUI_RGSKEY_PARAM1];
// 函数调用
__block BOOL result = NO;
Class HostEngineManagerClass = NSClassFromString(@"TXCHostEngineManager");
SEL sharedManagerSEL = NSSelectorFromString(@"sharedManager");
IMP sharedManagerIMP = [HostEngineManagerClass methodForSelector:sharedManagerSEL];
NSObject *(*sharedManagerFunc)(id, SEL) = (void *)sharedManagerIMP;
NSObject *sharedManagerObj = sharedManagerFunc(HostEngineManagerClass, sharedManagerSEL);
void (^SyncRequestToHostFuncBlock)(NSDictionary *) = ^(NSDictionary *outParams) {
NSObject *featureAuthObj = [outParams objectForKey:TUI_RETKEY_PARAM1];
if ([featureAuthObj isKindOfClass:[NSNumber class]]) {
result = [(NSNumber *)featureAuthObj boolValue];
}
};
SEL sendSyncRequestToHostSEL = NSSelectorFromString(@"sendSyncRequestToHostWithFunctionId:inputParams:completionHandler:");
IMP sendSyncRequestToHostIMP = [sharedManagerObj methodForSelector:sendSyncRequestToHostSEL];
void (*sendSyncRequestToHostFunc)(id, SEL, NSInteger, NSDictionary *, void (^)(NSDictionary<NSString *, NSObject *> *)) = (void *)sendSyncRequestToHostIMP;
sendSyncRequestToHostFunc(sharedManagerObj, sendSyncRequestToHostSEL, TUI_ID_CHECK_FEATURE_AUTH, inputParams, SyncRequestToHostFuncBlock);
return result;
}
@end
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXBACKPLAYER_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXBACKPLAYER_H_
#import <Foundation/Foundation.h>
#import <AVKit/AVKit.h>
#import "FTXPipPlayerDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@interface FTXBackPlayer : NSObject
@property(nonatomic, strong)id<FTXPipPlayerDelegate> playerDelegate;
- (void)prepareVideo:(AVPlayerItem *)item;
- (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item;
- (void)play;
- (void)setContainerView:(UIView*)container;
- (void)setLoopback:(BOOL)isLoop;
- (void)seekTo:(int64_t)positionMs;
- (void)stop;
- (void)pause;
- (AVPlayerLayer*)getPlayerLayer;
@end
NS_ASSUME_NONNULL_END
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXBACKPLAYER_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import "FTXBackPlayer.h"
#import "FTXPlayerConstants.h"
#import "FTXLog.h"
/// 微妙转毫秒
#define USEC_TO_MSEC 1000ull
static NSString* kPipTag = @"FTXBackPlayer";
static void *gTVKAVPlayerKVOContextTimeControlStatus = &gTVKAVPlayerKVOContextTimeControlStatus;
static void *gTVKAVPlayerKVOContextAirplay = &gTVKAVPlayerKVOContextAirplay;
static void *gTVKAVPlayerKVOContextError = &gTVKAVPlayerKVOContextError;
static void *gTVKAVPlayerItemKVOContextState = &gTVKAVPlayerItemKVOContextState;
@interface FTXBackPlayer()
@property (nonatomic, strong) AVPlayer* avPlayer;
@property (nonatomic, strong) AVPlayerLayer* playerLayer;
@property (nonatomic, strong) AVPlayerItem* playerItem;
@property (nonatomic, assign) BOOL isLoopBack;
@property (nonatomic, strong) AVAsset* asset;
@property (nonatomic, assign) FTXAVPlayerState playerState;/// 系统播放资源
@end
@implementation FTXBackPlayer
- (instancetype)init
{
self = [super init];
if (self) {
self.playerState = FTXAVPlayerStateIdle;
}
return self;
}
- (void)prepareVideo:(AVPlayerItem *)item {
self.asset = item.asset;
self.playerItem = item;
self.avPlayer = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];
self.avPlayer.volume = 0;
self.avPlayer.rate = 1;
self.avPlayer.muted = YES;
if (self.isLoopBack) {
[self.avPlayer setActionAtItemEnd:AVPlayerActionAtItemEndNone];
}
// 不允许airplay
self.avPlayer.allowsExternalPlayback = NO;
self.playerLayer = [[AVPlayerLayer alloc] init];
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.playerLayer.hidden = YES;
[self.playerLayer setPlayer:self.avPlayer];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackFinished:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:self.playerItem];
[self addKVOWithPlayer:self.avPlayer];
self.playerState = FTXAVPlayerStatePrepared;
}
- (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)playerItem {
FTXLOGI(@"[%@]replaceCurrentItemWithPlayerItem", kPipTag);
if (nil == self.avPlayer) {
FTXLOGW(@"[%@]replaceCurrentItemWithPlayerItem met null player", kPipTag);
return;
}
self.asset = playerItem.asset;
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
self.playerItem = playerItem;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackFinished:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:self.playerItem];
}
- (void)seekTo:(int64_t)positionMs {
FTXLOGI(@"[%@]seekto %lld ms", kPipTag, positionMs);
if (positionMs < 0) {
return;
}
if (nil == self.avPlayer) {
FTXLOGW(@"[%@]seekto met null player", kPipTag);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.avPlayer seekToTime:CMTimeMake(positionMs * 1000, USEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
});
}
- (void)play {
FTXLOGI(@"[%@]play", kPipTag);
if (nil == self.avPlayer) {
FTXLOGW(@"[%@]play met null player", kPipTag);
return;
}
[self.avPlayer play];
self.playerState = FTXAVPlayerStatePlaying;
}
- (void)pause {
FTXLOGI(@"[%@]pause", kPipTag);
if (nil == self.avPlayer) {
FTXLOGW(@"[%@]pause met null player", kPipTag);
return;
}
dispatch_async(dispatch_get_main_queue(),^{
[self.avPlayer pause];
self.playerState = FTXAVPlayerStatePaused;
});
}
- (void)stop {
FTXLOGI(@"[%@]stop", kPipTag);
if (nil == self.avPlayer) {
FTXLOGW(@"[%@]stop met null player", kPipTag);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.avPlayer pause];
self.playerState = FTXAVPlayerStateStopped;
});
[self reset];
}
- (void)setContainerView:(UIView*)container {
FTXLOGI(@"[%@]setContainerView", kPipTag);
if (nil == self.playerLayer) {
FTXLOGW(@"[%@]setContainerView met null playerLayer", kPipTag);
return;
}
[container.layer addSublayer:self.playerLayer];
self.playerLayer.frame = container.bounds;
}
- (void)setPlayerState:(FTXAVPlayerState)playerState {
FTXLOGI(@"[%@]setPlayerState,playerState:%ld", kPipTag, (long)playerState);
if (self.playerDelegate != nil) {
[self.playerDelegate playerStateDidChange:playerState];
}
self->_playerState = playerState;
}
- (void)setLoopback:(BOOL)isLoop {
FTXLOGI(@"[%@]setLoopback,isLoop:%i", kPipTag, isLoop);
self.isLoopBack = isLoop;
}
- (AVPlayerLayer *)getPlayerLayer {
return self.playerLayer;
}
- (void)reset {
[self.playerLayer removeFromSuperlayer];
[self.playerLayer setPlayer:nil];
[self removeKVOWithPlayer:self.avPlayer];
self.isLoopBack = NO;
self.playerState = FTXAVPlayerStateIdle;
self.avPlayer = nil;
self.playerLayer = nil;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.avPlayer) {
if (context == gTVKAVPlayerKVOContextTimeControlStatus) {
// 画中画播放时,才管系统播放器的播放暂停状态。 非画中画情况下,都是主播放器影响系统播放器,不需要系统播放器反过来影响主播放器
[self handleAVPlayerTimeControlStatusChanged];
}
}
}
- (int64_t)currentPositionMs {
return CMTimeGetSeconds(self.avPlayer.currentTime) * USEC_TO_MSEC;
}
- (int64_t)durationMs {
return (int64_t)(CMTimeGetSeconds(self.asset.duration) * 1000);
}
- (void)playbackFinished:(NSNotification *)notification {
if (self.isLoopBack) {
[self.avPlayer seekToTime:CMTimeMake(0, USEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[self.avPlayer play];
}
}
- (void)addKVOWithPlayer:(AVPlayer *)player {
if (player != nil) {
[player addObserver:self forKeyPath:@"error" options:NSKeyValueObservingOptionNew context:gTVKAVPlayerKVOContextError];
// kvo监听系统播放器的暂停播放状态(之前监听播放速度rate来判断状态,可能不准,监听timeControlStatus是最准的)
if (@available(iOS 10.0, macOS 10.12, *)) {
[player addObserver:self forKeyPath:@"timeControlStatus" options:NSKeyValueObservingOptionNew context:gTVKAVPlayerKVOContextTimeControlStatus];
} else {
[player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:gTVKAVPlayerKVOContextTimeControlStatus];
}
[player addObserver:self forKeyPath:@"airPlayVideoActive" options:NSKeyValueObservingOptionNew context:gTVKAVPlayerKVOContextAirplay];
[player addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:gTVKAVPlayerItemKVOContextState];
}
}
- (void)removeKVOWithPlayer:(AVPlayer *)player {
if (player != nil) {
[player removeObserver:self forKeyPath:@"error"];
// kvo监听系统播放器的暂停播放状态(之前监听播放速度rate来判断状态,可能不准,监听timeControlStatus是最准的)
if (@available(iOS 10.0, macOS 10.12, *)) {
[player removeObserver:self forKeyPath:@"timeControlStatus"];
} else {
[player removeObserver:self forKeyPath:@"rate"];
}
[player removeObserver:self forKeyPath:@"airPlayVideoActive"];
[player removeObserver:self forKeyPath:@"status"];
}
}
- (void)handleAVPlayerTimeControlStatusChanged {
BOOL isPlayingState = NO;
if (@available(iOS 10.0, macOS 10.12, *)) {
isPlayingState = self.avPlayer.timeControlStatus == AVPlayerTimeControlStatusPlaying
|| self.avPlayer.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate;
} else {
isPlayingState = (self.avPlayer.rate != 0);
}
if (isPlayingState && self.playerState != FTXAVPlayerStatePlaying) {
FTXLOGI(@"[%@]playerStateDidChange:playing", kPipTag);
self.playerState = FTXAVPlayerStatePlaying;
} else if (!isPlayingState && self.playerState == FTXAVPlayerStatePlaying) {
FTXLOGI(@"[%@]playerStateDidChange:paused", kPipTag);
// 换源之后,播放完成前一刻,监听系统播放器TimeControlStatus属性变为暂停状态,这不是用户调用的暂停,不需要回抛暂停
if (self.currentPositionMs == self.durationMs) {
FTXLOGI(@"[%@]playerStateDidChange:complete, duration:%lld == currentPosition:%lld",
kPipTag, self.durationMs, self.currentPositionMs);
return;
}
self.playerState = FTXAVPlayerStatePaused;
}
}
@end
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPCALLER_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPCALLER_H_
#import "FTXLiteAVSDKHeader.h"
#import "FTXLivePipDelegate.h"
#import "FTXPipPlayerDelegate.h"
#import "FTXPipRenderView.h"
@protocol FTXPipCaller <NSObject>
@property(nonatomic, strong)id<FTXLivePipDelegate> pipDelegate;
@property(nonatomic, strong)id<FTXPipPlayerDelegate> playerDelegate;
- (int)handleStartPip:(CGSize)size;
- (void)exitPip;
- (FTXPipRenderView*)getVideoView;
- (TX_VOD_PLAYER_PIP_STATE)getStatus;
- (void)pausePipVideo;
- (void)resumePipVideo;
- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer;
@end
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPCALLER_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPCONTROLLER_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPCONTROLLER_H_
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "FTXLiteAVSDKHeader.h"
#import "FTXLivePipDelegate.h"
#import "FTXPipPlayerDelegate.h"
@interface FTXPipController : NSObject
@property(nonatomic, strong)id<FTXLivePipDelegate> pipDelegate;
@property(nonatomic, strong)id<FTXPipPlayerDelegate> playerDelegate;
+ (instancetype)shareInstance;
- (int)startOpenPip:(V2TXLivePlayer*)livePlayer withSize:(CGSize)size;
- (void)pausePipVideo;
- (void)resumePipVideo;
- (void)exitPip;
- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer;
@end
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPCONTROLLER_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
#import "FTXPipController.h"
#import "FTXEvent.h"
#import "FTXPipCaller.h"
#import "FTXPipFactory.h"
#import "TXPipAuth.h"
#import "FTXLog.h"
#import <CoreVideo/CoreVideo.h>
@interface FTXPipController()
@property (nonatomic, strong) id<FTXPipCaller> pipImpl;
@property (nonatomic, strong) FTXPipFactory* pipFactory;
@property (atomic, strong) NSObject* controlLock;
@end
static NSString* kPipTag = @"FTXPipController";
static FTXPipController *_shareInstance = nil;
@implementation FTXPipController
+ (instancetype)shareInstance {
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
_shareInstance = [[FTXPipController alloc] init];
});
return _shareInstance;
}
- (instancetype)init
{
self = [super init];
if (self) {
self.pipFactory = [[FTXPipFactory alloc] init];
self.controlLock = [[NSObject alloc] init];
}
return self;
}
- (int)startOpenPip:(V2TXLivePlayer *)livePlayer withSize:(CGSize)size{
if (![TXPipAuth cpa]) {
FTXLOGE(@"%@ pip auth is deined when enter", kPipTag);
if (nil != self.pipDelegate) {
[self.pipDelegate pictureInPictureErrorDidOccur:FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_AUTH_DENIED];
}
return ERROR_PIP_AUTH_DENIED;
}
if (![TXVodPlayer isSupportPictureInPicture]) {
FTXLOGE(@"%@ pip is not support", kPipTag);
if (nil != self.pipDelegate) {
[self.pipDelegate pictureInPictureErrorDidOccur:FTX_VOD_PLAYER_PIP_ERROR_TYPE_DEVICE_NOT_SUPPORT];
}
return ERROR_IOS_PIP_DEVICE_NOT_SUPPORT;
}
if (nil != self.pipImpl) {
TX_VOD_PLAYER_PIP_STATE status = [self.pipImpl getStatus];
if (status == TX_VOD_PLAYER_PIP_STATE_WILL_START
|| status == TX_VOD_PLAYER_PIP_STATE_DID_START) {
FTXLOGE(@"%@ pip is running when enter, status %ld", kPipTag, (long)status);
if (nil != self.pipDelegate) {
[self.pipDelegate pictureInPictureErrorDidOccur:FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_IS_RUNNING];
}
return ERROR_IOS_PIP_IS_RUNNING;
}
}
self.pipImpl = [self.pipFactory createPipCaller];
if (self.pipDelegate != nil) {
self.pipImpl.pipDelegate = self.pipDelegate;
}
if (self.playerDelegate != nil) {
self.pipImpl.playerDelegate = self.playerDelegate;
}
int retCode = [self.pipImpl handleStartPip:size];
return retCode;
}
- (void)setPipDelegate:(id<FTXLivePipDelegate>)pipDelegate {
if (nil != self.pipImpl) {
self.pipImpl.pipDelegate = pipDelegate;
}
self->_pipDelegate = pipDelegate;
}
- (void)setPlayerDelegate:(id<FTXPipPlayerDelegate>)playerDelegate {
if (nil != self.pipImpl) {
self.pipImpl.playerDelegate = playerDelegate;
}
self->_playerDelegate = playerDelegate;
}
- (void)exitPip {
@synchronized (self.controlLock) {
if (nil != self.pipImpl) {
[self.pipImpl exitPip];
self.pipImpl = nil;
self.pipDelegate = nil;
self.playerDelegate = nil;
}
}
}
- (void)pausePipVideo {
if (nil != self.pipImpl) {
[self.pipImpl pausePipVideo];
}
}
- (void)resumePipVideo {
if (nil != self.pipImpl) {
[self.pipImpl resumePipVideo];
}
}
- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer {
@synchronized (self.controlLock) {
if (!pixelBuffer || pixelBuffer == NULL) {
NSLog(@"Invalid CVPixelBufferRef");
return;
}
if (nil != self.pipImpl) {
[self.pipImpl displayPixelBuffer:pixelBuffer];
}
}
}
@end
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPFACTORY_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPFACTORY_H_
#import <Foundation/Foundation.h>
#import "FTXPipCaller.h"
NS_ASSUME_NONNULL_BEGIN
@interface FTXPipFactory : NSObject
- (id<FTXPipCaller>)createPipCaller;
@end
NS_ASSUME_NONNULL_END
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPFACTORY_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import "FTXPipFactory.h"
#import "FTXPipGlobalImpl.h"
@implementation FTXPipFactory
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
- (nonnull id<FTXPipCaller>)createPipCaller {
id<FTXPipCaller> pipCalled = [[FTXPipGlobalImpl alloc] init];
return pipCalled;
}
@end
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPGLOBALIMPL_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPGLOBALIMPL_H_
#import <Foundation/Foundation.h>
#import "FTXPipCaller.h"
NS_ASSUME_NONNULL_BEGIN
@interface FTXPipGlobalImpl : NSObject<FTXPipCaller>
@end
NS_ASSUME_NONNULL_END
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPGLOBALIMPL_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import "FTXPipGlobalImpl.h"
#import "TXPipAuth.h"
#import <AVKit/AVKit.h>
#import "FTXEvent.h"
#import "FTXLog.h"
#import "FTXPlayerConstants.h"
#import "FTXPipRenderView.h"
#import "FTXBackPlayer.h"
@interface FTXPipGlobalImpl()<AVPictureInPictureControllerDelegate>
@property (nonatomic, strong)AVPictureInPictureController *pipController;
@property (nonatomic, assign)TX_VOD_PLAYER_PIP_STATE pipStatus;
@property (nonatomic, assign)FTX_LIVE_PIP_ERROR pipError;
/// app传入的videoView在父view上的约束,恢复view的时候要重新加上之前的约束
@property (nonatomic, strong)NSMutableArray *constraintArray;
@property (nonatomic, strong)UIView* backgroundPlayerView;
/// 传入的videoView在移动view(coverVideoViewToPIPView)之前的父view
@property (nonatomic, weak)UIView* superViewOfVideoView;
@property (nonatomic, strong)FTXPipRenderView* videoView;
@property (nonatomic, strong) NSArray *tempConstraintArray;
@property (nonatomic, strong) FTXBackPlayer* backPlayer;
@end
static NSString* kPipTag = @"FTXPipCaller";
@implementation FTXPipGlobalImpl
@synthesize pipDelegate;
@synthesize playerDelegate;
- (instancetype)init
{
self = [super init];
if (self) {
self.pipStatus = TX_VOD_PLAYER_PIP_STATE_UNDEFINED;
self.pipError = FTX_VOD_PLAYER_PIP_ERROR_TYPE_NONE;
self.constraintArray = [[NSMutableArray alloc] init];
self.videoView = [self createVideoView];
}
return self;
}
- (int)handleStartPip:(CGSize)size {
if (![TXPipAuth cpa]) {
FTXLOGE(@"%@ pip auth is deined when handle", kPipTag);
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_UNDEFINED];
[self onPipError:FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_AUTH_DENIED];
return ERROR_PIP_AUTH_DENIED;
}
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *resourcePath = [mainBundle pathForResource:@"tx_vod_seamless_pip_backgroud_video" ofType:@"mp4" inDirectory:@"TXVodPlayer.bundle"];
// 获取文件管理器
NSFileManager *fileManager = [NSFileManager defaultManager];
// 判断资源是否存在
BOOL resourceExists = [fileManager fileExistsAtPath:resourcePath];
if (resourceExists) {
FTXLOGI(@"%@ Resource exists at path: %@", kPipTag, resourcePath);
[self prepareWithURL:[NSURL fileURLWithPath:resourcePath] withSize:size withDurationSec:100
isReplace:NO];
} else {
FTXLOGE(@"%@ Resource does not exist at path: %@", kPipTag, resourcePath);
[self onPipError:FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_MISS_RESOURCE];
}
return NO_ERROR;
}
- (void)exitPip {
if (![TXPipAuth cpa]) {
FTXLOGE(@"%@ pip auth is deined when closed", kPipTag);
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_UNDEFINED];
[self onPipError:FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_AUTH_DENIED];
return;
}
if (nil != self.pipController) {
[self.pipController stopPictureInPicture];
}
if (self.backgroundPlayerView && self.backgroundPlayerView.superview) {
[self.backgroundPlayerView removeFromSuperview];
self.backgroundPlayerView = nil;
}
if (self.backPlayer) {
// 停止播放并释放资源
[self.backPlayer stop];
}
}
- (FTXPipRenderView *)getVideoView {
return self.videoView;
}
- (TX_VOD_PLAYER_PIP_STATE)getStatus {
return self.pipStatus;
}
- (void)pausePipVideo {
if (nil != self.backPlayer) {
[self.backPlayer pause];
}
}
- (void)resumePipVideo {
if (nil != self.backPlayer) {
[self.backPlayer play];
}
}
- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer {
[self.videoView displayPixelBuffer:pixelBuffer];
}
#pragma mark - AVPictureInPictureControllerDelegate
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
if ([TXPipAuth cpa]) {
FTXLOGI(@"%@ pictureInPictureControllerWillStartPictureInPicture", kPipTag);
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_WILL_START];
UIView* pipView = self.pipView;
FTXPipRenderView* videoView = self.videoView;
if (!pipView) {
FTXLOGE(@"[%@] coverVideoViewToPIPView, pipView is nil, videoView is: %p", kPipTag, videoView);
return;
}
if (!videoView) {
FTXLOGE(@"[%@] coverVideoViewToPIPView, videoView is nil", kPipTag);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// 把当前videoView和父view记下来
self.superViewOfVideoView = videoView.superview;
// 把当前videoView在其父view上的约束记下来,等恢复的时候重新加上这些约束
[self.constraintArray removeAllObjects]; // 移除旧的约束
for (NSLayoutConstraint *constraint in self.superViewOfVideoView.constraints) {
if (constraint.firstItem == self.videoView) {
[self.constraintArray addObject:constraint];
}
}
videoView.translatesAutoresizingMaskIntoConstraints = NO;
[videoView removeFromSuperview];
[pipView addSubview:self.videoView];
// 添加约束 使videoview撑满pipview
NSLayoutConstraint *contraintTop = [NSLayoutConstraint constraintWithItem:videoView
attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual
toItem:pipView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
NSLayoutConstraint *contraintLeft = [NSLayoutConstraint constraintWithItem:videoView
attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual
toItem:pipView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0];
NSLayoutConstraint *contraintBottom = [NSLayoutConstraint constraintWithItem:videoView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:pipView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0];
NSLayoutConstraint *contraintRight = [NSLayoutConstraint constraintWithItem:videoView
attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual
toItem:pipView
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0];
// 把约束添加到父视图pipview上
self.tempConstraintArray = [NSArray arrayWithObjects:contraintTop, contraintLeft, contraintBottom, contraintRight, nil];
[NSLayoutConstraint activateConstraints:self.tempConstraintArray]; // activateConstraints 效率更高
[videoView setNeedsLayout];
[videoView layoutIfNeeded];
FTXLOGI(@"[%@] coverVideoViewToPIPView finished, videoView's superview is: %p", kPipTag, videoView.superview);
});
} else {
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_UNDEFINED];
[self onPipError:FTX_VOD_PLAYER_PIP_ERROR_TYPE_PIP_AUTH_DENIED];
FTXLOGE(@"%@ pip auth is deined when opened", kPipTag);
}
}
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
FTXLOGI(@"%@ pictureInPictureControllerDidStartPictureInPicture", kPipTag);
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_DID_START];
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error {
FTXLOGI(@"%@ failedToStartPictureInPictureWithError:%@", kPipTag, error);
}
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
FTXLOGI(@"%@ pictureInPictureControllerWillStopPictureInPicture", kPipTag);
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self.pipController removeObserver:self forKeyPath:@"pictureInPicturePossible"];
[self.backPlayer stop];
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_WILL_STOP];
[self exitPip];
}
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
FTXLOGI(@"%@ pictureInPictureControllerDidStopPictureInPicture", kPipTag);
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_DID_STOP];
}
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler {
FTXLOGI(@"%@ restoreUserInterfaceForPictureInPictureStopWithCompletionHandler", kPipTag);
[self changeStatus:TX_VOD_PLAYER_PIP_STATE_RESTORE_UI];
}
#pragma mark - private method
- (FTXPipRenderView *)createVideoView {
if (!_videoView) {
// Set the size to 1 pixel to ensure proper display in PIP.
_videoView = [[FTXPipRenderView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
}
return _videoView;
}
- (void)changeStatus:(TX_VOD_PLAYER_PIP_STATE)status{
self.pipStatus = status;
FTXLOGE(@"%@ pip met status changed %ld", kPipTag, status);
if (self.pipDelegate) {
[self.pipDelegate pictureInPictureStateDidChange:status];
}
}
- (void)onPipError:(FTX_LIVE_PIP_ERROR)error{
self.pipError = error;
FTXLOGE(@"%@ pip met error %ld", kPipTag, error);
if (self.pipDelegate) {
[self.pipDelegate pictureInPictureErrorDidOccur:error];
}
}
/// 获取系统画中画view
- (UIView *)pipView {
///画中画view是windos列表里最后一个PGHostWindow类型的view。单实例的情况下,就是列表里第一个view,多实例的情况下,要取最后一个PGHostWindow类型的view
UIView *pipView = [UIApplication sharedApplication].windows.firstObject;
Class pgHostWindowClass = NSClassFromString(@"PGHostedWindow");
if (!pgHostWindowClass) {
return pipView;
}
// 取最后一个PGHostWindow类型的view
for (UIView *view in [UIApplication sharedApplication].windows) {
if ([view isKindOfClass:pgHostWindowClass]) {
pipView = view;
}
}
return pipView;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.pipController) {
if ([keyPath isEqualToString:@"pictureInPicturePossible"]) {
if (self.pipController.pictureInPicturePossible && !self.pipController.pictureInPictureActive) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.pipController.isPictureInPictureActive) {
[self.pipController stopPictureInPicture];
} else {
[self.pipController startPictureInPicture];
}
});
}
}
}
}
/// 1 将输入视频url, resize到指定大小,2 拼接到指定长度,3 用新的视频资源初始化avplayer(3需要在主线程中做,但耗时非常少可以忽略)
/// @param inputURL 输入视频url
/// @param size 指定视频尺寸
/// @param durationSec 指定视频时长
/// @param isReplace 是否是换源 换源的话只换播放资源 不重新创建播放器
- (void)prepareWithURL:(NSURL *)inputURL withSize:(CGSize)size withDurationSec:(NSTimeInterval)durationSec isReplace:(BOOL)isReplace {
if (!inputURL) {
return;
}
AVAsset *videoAsset = [[AVURLAsset alloc] initWithURL:inputURL options:nil];
AVMutableComposition *composition = [AVMutableComposition composition];
// 视频类型的的Track,这个方法里只添加视频track,不需要音频
AVMutableCompositionTrack *compositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:kCMPersistentTrackID_Invalid];
// 拼接视频
int localVideoDurationSec = CMTimeGetSeconds(videoAsset.duration); // 本地视频时长
if (localVideoDurationSec == 0) {
FTXLOGW(@"%@ prepareWithURL failed, local video duration is 0, return", kPipTag);
return;
}
CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration);
int counts = (durationSec / localVideoDurationSec);
for (int i = 0; i < counts; i++) {
[compositionTrack insertTimeRange:timeRange ofTrack:[videoAsset tracksWithMediaType:AVMediaTypeVideo][0] atTime:kCMTimeZero error:nil];
}
// 拼最后一段视频
int lastVideoDurationSec = (int)durationSec % localVideoDurationSec;
if (lastVideoDurationSec != 0) {
CMTime lastVideoTime = CMTimeMake(lastVideoDurationSec, 1);
CMTimeRange lastVideoTimeRange = CMTimeRangeMake(kCMTimeZero, lastVideoTime);
[compositionTrack insertTimeRange:lastVideoTimeRange
ofTrack:[videoAsset tracksWithMediaType:AVMediaTypeVideo][0]
atTime:kCMTimeZero error:nil];
}
// resize
AVMutableVideoCompositionLayerInstruction *videoCompositionLayerIns =
[AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionTrack];
[videoCompositionLayerIns setTransform:compositionTrack.preferredTransform atTime:kCMTimeZero]; //得到视频素材
AVMutableVideoCompositionInstruction *videoCompositionIns = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
[videoCompositionIns setTimeRange:CMTimeRangeMake(kCMTimeZero, compositionTrack.timeRange.duration)];
//得到视频轨道
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = @[videoCompositionIns];
if (size.width <= 0 || size.height <= 0) {
FTXLOGE(@"%@ prepareWithURL failed, wrong video size, bgPlayer: %p, return ", kPipTag, self.backPlayer);
return;
}
videoComposition.renderSize = size; //指定尺寸
videoComposition.frameDuration = CMTimeMake(2, 2);
// 视频裁剪拼接成功后开始prepare
NSArray *requestedKeys = @[@"playable"];
[composition loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
// NOTE:移除旧的PlayerView,防止旧BG页面残留
if (self.backgroundPlayerView && self.backgroundPlayerView.superview) {
[self.backgroundPlayerView removeFromSuperview];
self.backgroundPlayerView = nil;
}
self.backgroundPlayerView = [[UIView alloc] initWithFrame:CGRectZero];
self.backgroundPlayerView.backgroundColor = [UIColor clearColor];
self.backgroundPlayerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.backgroundPlayerView.frame = self.pipView.bounds;
AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithAsset:composition];
playerItem.videoComposition = videoComposition;
if (isReplace && self.backPlayer != nil) {
[self.backPlayer replaceCurrentItemWithPlayerItem:playerItem];
[self.backPlayer play];
} else {
self.backPlayer = [[FTXBackPlayer alloc] init];
[self.backPlayer prepareVideo:playerItem];
[self.backPlayer setContainerView:self.backgroundPlayerView];
[self.backPlayer getPlayerLayer].frame = self.videoView.bounds;
[self.pipView addSubview:self.backgroundPlayerView];
self.backPlayer.playerDelegate = self.playerDelegate;
[self.backPlayer setLoopback:YES];
[self.backPlayer play];
self.pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:[self.backPlayer getPlayerLayer]];
self.pipController.delegate = self;
// 使用 KVC,隐藏播放按钮、快进快退按钮
[self.pipController setValue:[NSNumber numberWithInt:1] forKey:@"controlsStyle"];
[self setRequiresLinearPlayback:YES];
[self.pipController addObserver:self forKeyPath:@"pictureInPicturePossible" options:NSKeyValueObservingOptionNew context:nil];
}
});
}];
}
- (void)setRequiresLinearPlayback:(BOOL)requiresLinearPlayback {
if (@available(iOS 14.0, macOS 11.0, *)) {
//requiresLinearPlayback: NO:画中画小窗会显示快进快退按钮 YES:不会显示快进快退按钮
self.pipController.requiresLinearPlayback = requiresLinearPlayback;
}
}
- (void)setPlayerDelegate:(id<FTXPipPlayerDelegate>)playerDelegate {
if (self.backPlayer != nil) {
self.backPlayer.playerDelegate = playerDelegate;
}
self->playerDelegate = playerDelegate;
}
@end
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPPLAYERDELEGATE_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPPLAYERDELEGATE_H_
#import "FTXPlayerConstants.h"
@protocol FTXPipPlayerDelegate <NSObject>
-(void)playerStateDidChange:(FTXAVPlayerState)playerState;
@end
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPPLAYERDELEGATE_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPRENDERVIEW_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPRENDERVIEW_H_
#import <Foundation/Foundation.h>
#import "FTXBackPlayer.h"
NS_ASSUME_NONNULL_BEGIN
@interface FTXPipRenderView : UIView
- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer;
@end
NS_ASSUME_NONNULL_END
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_LIVE_PIP_FTXPIPRENDERVIEW_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import "FTXPipRenderView.h"
#import "FTXImgTools.h"
@interface FTXPipRenderView()
@property (nonatomic, strong) CALayer *videoLayer;
@end
@implementation FTXPipRenderView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.videoLayer = [CALayer layer];
self.videoLayer.frame = self.bounds;
[self.layer addSublayer:self.videoLayer];
}
return self;
}
- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer {
CIImage *ciImage = [FTXImgTools ciImageFromPixelBuffer:pixelBuffer];
// 创建一个CIContext
CIContext *context = [CIContext contextWithOptions:nil];
// 将CIImage渲染到CGImage
CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
// 更新CALayer的内容
self.videoLayer.contents = (__bridge id _Nullable)(cgImage);
// 释放CGImage
CGImageRelease(cgImage);
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize oldSize = self.videoLayer.frame.size;
CGRect newRect = self.frame;
CGSize newSize = newRect.size;
if (oldSize.width != newSize.width || oldSize.height != newSize.height) {
self.videoLayer.frame = newRect;
}
}
@end
// Copyright (c) 2024 Tencent. All rights reserved.
#ifndef SUPERPLAYER_FLUTTER_IOS_CLASSES_TOOLS_FTXIMGTOOLS_H_
#define SUPERPLAYER_FLUTTER_IOS_CLASSES_TOOLS_FTXIMGTOOLS_H_
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FTXImgTools : NSObject
+ (CVPixelBufferRef)CVPixelBufferRefFromUiImage:(UIImage *)img;
+ (CIImage *)ciImageFromPixelBuffer:(CVPixelBufferRef)pixelBuffer;
@end
NS_ASSUME_NONNULL_END
#endif // SUPERPLAYER_FLUTTER_IOS_CLASSES_TOOLS_FTXIMGTOOLS_H_
// Copyright (c) 2024 Tencent. All rights reserved.
#import "FTXImgTools.h"
@implementation FTXImgTools
+ (CVPixelBufferRef)CVPixelBufferRefFromUiImage:(UIImage *)img {
CGSize size = img.size;
CGImageRef image = [img CGImage];
BOOL hasAlpha = CGImageRefContainsAlpha(image);
CFDictionaryRef empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
empty, kCVPixelBufferIOSurfacePropertiesKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, inputPixelFormat(), (__bridge CFDictionaryRef) options, &pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
uint32_t bitmapInfo = bitmapInfoWithPixelFormatType(inputPixelFormat(), (bool)hasAlpha);
CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, CVPixelBufferGetBytesPerRow(pxbuffer), rgbColorSpace, bitmapInfo);
NSParameterAssert(context);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
return pxbuffer;
}
+ (CIImage *)ciImageFromPixelBuffer:(CVPixelBufferRef)pixelBuffer {
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
return ciImage;
}
static OSType inputPixelFormat(void){
return kCVPixelFormatType_32BGRA;
}
static uint32_t bitmapInfoWithPixelFormatType(OSType inputPixelFormat, bool hasAlpha){
if (inputPixelFormat == kCVPixelFormatType_32BGRA) {
uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
if (!hasAlpha) {
bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host;
}
return bitmapInfo;
}else if (inputPixelFormat == kCVPixelFormatType_32ARGB) {
uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big;
return bitmapInfo;
}else{
return 0;
}
}
// Check alpha value
BOOL CGImageRefContainsAlpha(CGImageRef imageRef) {
if (!imageRef) {
return NO;
}
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}
@end
...@@ -281,20 +281,13 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T ...@@ -281,20 +281,13 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
{String? backIconForAndroid, String? playIconForAndroid, String? pauseIconForAndroid, String? forwardIconForAndroid}) async { {String? backIconForAndroid, String? playIconForAndroid, String? pauseIconForAndroid, String? forwardIconForAndroid}) async {
if (_isNeedDisposed) return -1; if (_isNeedDisposed) return -1;
await _initPlayer.future; await _initPlayer.future;
if (defaultTargetPlatform == TargetPlatform.android) { IntMsg intMsg = await _livePlayerApi.enterPictureInPictureMode(PipParamsPlayerMsg()
IntMsg intMsg = await _livePlayerApi.enterPictureInPictureMode(PipParamsPlayerMsg() ..backIconForAndroid = backIconForAndroid
..backIconForAndroid = backIconForAndroid ..playIconForAndroid = playIconForAndroid
..playIconForAndroid = playIconForAndroid ..pauseIconForAndroid = pauseIconForAndroid
..pauseIconForAndroid = pauseIconForAndroid ..forwardIconForAndroid = forwardIconForAndroid
..forwardIconForAndroid = forwardIconForAndroid ..playerId = _playerId);
..playerId = _playerId); return intMsg.value ?? -1;
return intMsg.value ?? -1;
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
// The background picture-in-picture feature for ios steaming live is temporarily disabled.
return -1;
} else {
return -1;
}
} }
/// Exit picture-in-picture mode if the player is in picture-in-picture mode. /// Exit picture-in-picture mode if the player is in picture-in-picture mode.
......
...@@ -374,6 +374,12 @@ abstract class TXVodPlayEvent { ...@@ -374,6 +374,12 @@ abstract class TXVodPlayEvent {
// PIP error, PIP function is not started (only support iOS). // PIP error, PIP function is not started (only support iOS).
// pip 错误,PIP功能没有启动 only support iOS // pip 错误,PIP功能没有启动 only support iOS
static const ERROR_IOS_PIP_NOT_RUNNING = -111; static const ERROR_IOS_PIP_NOT_RUNNING = -111;
// PIP start time out
// PIP 启动超时
static const ERROR_IOS_PIP_START_TIME_OUT = -112;
// Insufficient permissions, currently only appears in Picture-in-Picture live streaming
// 权限不足,目前只出现在直播画中画
static const ERROR_PIP_AUTH_DENIED = -201;
// PIP error, currently unable to enter PIP mode, such as being in full screen mode. // PIP error, currently unable to enter PIP mode, such as being in full screen mode.
// pip 错误,当前不能进入pip模式,例如正处于全屏模式下 // pip 错误,当前不能进入pip模式,例如正处于全屏模式下
static const ERROR_PIP_CAN_NOT_ENTER = -120; static const ERROR_PIP_CAN_NOT_ENTER = -120;
......
...@@ -822,24 +822,11 @@ class SuperPlayerController { ...@@ -822,24 +822,11 @@ class SuperPlayerController {
Future<int> enterPictureInPictureMode( Future<int> enterPictureInPictureMode(
{String? backIcon, String? playIcon, String? pauseIcon, String? forwardIcon}) async { {String? backIcon, String? playIcon, String? pauseIcon, String? forwardIcon}) async {
if (_playerUIStatus == SuperPlayerUIStatus.WINDOW_MODE) { if (_playerUIStatus == SuperPlayerUIStatus.WINDOW_MODE) {
if (playerType == SuperPlayerType.VOD) { return TXPipController.instance.enterPip(getCurrentController(), _context,
return TXPipController.instance.enterPip(_vodPlayerController, _context, backIconForAndroid: backIcon,
backIconForAndroid: backIcon, playIconForAndroid: playIcon,
playIconForAndroid: playIcon, pauseIconForAndroid: pauseIcon,
pauseIconForAndroid: pauseIcon, forwardIconForAndroid: forwardIcon);
forwardIconForAndroid: forwardIcon);
} else {
if (defaultTargetPlatform == TargetPlatform.android) {
return TXPipController.instance.enterPip(_livePlayerController, _context,
backIconForAndroid: backIcon,
playIconForAndroid: playIcon,
pauseIconForAndroid: pauseIcon,
forwardIconForAndroid: forwardIcon);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
TXPipController.instance.exitAndReleaseCurrentPip();
return _livePlayerController.enterPictureInPictureMode();
}
}
} }
return TXVodPlayEvent.ERROR_PIP_CAN_NOT_ENTER; return TXVodPlayEvent.ERROR_PIP_CAN_NOT_ENTER;
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论