325 lines
9.3 KiB
Objective-C
325 lines
9.3 KiB
Objective-C
//
|
||
// SCRtmpLiveViewController.m
|
||
// SellyCloudSDK_Example
|
||
//
|
||
// Created by Caleb on 8/7/25.
|
||
// Copyright © 2025 Caleb. All rights reserved.
|
||
//
|
||
|
||
#import "SCLivePusherViewController.h"
|
||
#import <SellyCloudSDK/SellyCloudManager.h>
|
||
#import "SCLiveItemContainerView.h"
|
||
#import "FUManager.h"
|
||
#import <Photos/Photos.h>
|
||
#import "SCLiveStatsView.h"
|
||
#import "UIView+SellyCloud.h"
|
||
|
||
@interface SCLivePusherViewController ()<SellyPusherManagerDelegate>
|
||
@property (nonatomic, strong)UIView *liveView;
|
||
@property (nonatomic, strong)SellyVideoPusher *livePusher;
|
||
|
||
@property (nonatomic, strong)SCLiveItemContainerView *itemContainer;
|
||
@property (nonatomic, strong)SCLiveStatsView *statsView;
|
||
@end
|
||
|
||
@implementation SCLivePusherViewController
|
||
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
self.view.backgroundColor = UIColor.whiteColor;
|
||
[self.view addSubview:self.liveView];
|
||
[self.liveView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.view);
|
||
}];
|
||
// Do any additional setup after loading the view.
|
||
[self startLive];
|
||
// 禁止息屏
|
||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||
|
||
// [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(appDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
||
// [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(appWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
|
||
|
||
[self.view addSubview:self.statsView];
|
||
[self.statsView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.offset(10);
|
||
make.top.offset(90);
|
||
make.width.offset(210);
|
||
make.height.offset(220);
|
||
}];
|
||
}
|
||
|
||
- (void)appDidEnterBackground {
|
||
[self _sendPicture];
|
||
}
|
||
|
||
- (void)appWillEnterForeground {
|
||
[self _stopSendPicture];
|
||
}
|
||
|
||
- (void)startLive {
|
||
if (self.streamInfo) {
|
||
self.livePusher = [[SellyVideoPusher alloc] initWithStreamInfo:self.streamInfo];
|
||
}
|
||
else {
|
||
self.livePusher = [[SellyVideoPusher alloc] initWithUrl:self.url];
|
||
}
|
||
self.livePusher.preview = self.liveView;
|
||
self.livePusher.delegate = self;
|
||
self.livePusher.enableCustomVideoProcess = true;
|
||
self.livePusher.scaleMode = SellyPlayerScalingModeAspectFit;
|
||
|
||
SellyLiveVideoConfiguration *videoConfig = self.videoConfig;
|
||
#warning RTC直播中支持自动切换横竖屏,RTMP不支持
|
||
{
|
||
//横屏直播
|
||
// videoConfig.videoSize = CGSizeMake(1280, 720);
|
||
// videoConfig.outputImageOrientation = UIInterfaceOrientationLandscapeRight;
|
||
}
|
||
{
|
||
//竖屏直播
|
||
// videoConfig.videoSize = CGSizeMake(720, 1280);
|
||
// videoConfig.outputImageOrientation = UIInterfaceOrientationPortrait;
|
||
}
|
||
if (self.audioOnly) {
|
||
//纯语音直播
|
||
[self.livePusher startRunningAudio:nil];
|
||
}
|
||
else {
|
||
//音视频直播
|
||
[self.livePusher startRunning:AVCaptureDevicePositionBack videoConfig:videoConfig audioConfig:nil];
|
||
}
|
||
|
||
NSError *error = [self.livePusher startLive];
|
||
if (error) {
|
||
NSLog(@"###startLive failed. error == %@",error.localizedDescription);
|
||
}
|
||
|
||
__weak typeof(self) weakSelf = self;
|
||
NSMutableArray *items = NSMutableArray.new;
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = 1;
|
||
model.title = @"前/后";
|
||
model.clickCallback = ^{
|
||
[weakSelf switchDevicePosition];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = 2;
|
||
model.title = @"mute";
|
||
model.clickCallback = ^{
|
||
[weakSelf muteClick];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = 3;
|
||
model.title = @"camera";
|
||
model.clickCallback = ^{
|
||
[weakSelf cameraClick];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = 4;
|
||
model.title = @"镜像";
|
||
model.clickCallback = ^{
|
||
[weakSelf mirrorClick];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = 5;
|
||
model.title = @"截图";
|
||
model.clickCallback = ^{
|
||
[weakSelf captureImageClick];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = 6;
|
||
model.title = @"静图";
|
||
model.clickCallback = ^{
|
||
[weakSelf sendStaticImage];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
self.itemContainer.models = items;
|
||
[self.view addSubview:self.itemContainer];
|
||
[self.itemContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.offset(0);
|
||
make.bottom.offset(-48);
|
||
make.height.offset(100);
|
||
}];
|
||
}
|
||
|
||
- (void)switchDevicePosition {
|
||
[self.livePusher switchCameraPosition:nil];
|
||
if (self.livePusher.captureDevicePosition == AVCaptureDevicePositionFront) {
|
||
self.livePusher.mirror = true;
|
||
}
|
||
else {
|
||
self.livePusher.mirror = false;
|
||
}
|
||
}
|
||
|
||
- (void)muteClick {
|
||
if (self.livePusher.isMute) {
|
||
[self.livePusher startMicrophone];
|
||
}
|
||
else {
|
||
[self.livePusher stopMicrophone];
|
||
}
|
||
}
|
||
|
||
- (void)cameraClick {
|
||
if (self.livePusher.isCameraEnable) {
|
||
[self.livePusher stopCamera];
|
||
[self _sendPicture];
|
||
}
|
||
else {
|
||
[self _stopSendPicture];
|
||
[self.livePusher startCamera];
|
||
}
|
||
}
|
||
|
||
- (void)mirrorClick {
|
||
self.livePusher.mirror = !self.livePusher.mirror;
|
||
}
|
||
|
||
- (void)captureImageClick {
|
||
[self saveCurrentFrameToPhotoAlbum:[self.livePusher getCurrentImage]];
|
||
}
|
||
|
||
- (void)sendStaticImage {
|
||
if (self.livePusher.isCameraEnable) {
|
||
//推送静态图片
|
||
[self.livePusher stopCamera];
|
||
[self _sendPicture];
|
||
}
|
||
else {
|
||
//推送相机采集流
|
||
[self _stopSendPicture];
|
||
[self.livePusher startCamera];
|
||
}
|
||
}
|
||
|
||
- (void)_sendPicture {
|
||
[self.livePusher pushStaticImage:[UIImage imageNamed:@"test.jpg"]];
|
||
}
|
||
|
||
- (void)_stopSendPicture {
|
||
[self.livePusher stopPushImage];
|
||
}
|
||
|
||
- (void)viewDidDisappear:(BOOL)animated {
|
||
[super viewDidDisappear:animated];
|
||
[self.livePusher stopLive:^(NSError * _Nonnull error) {
|
||
|
||
}];
|
||
}
|
||
|
||
// 假设 self.session 是你的 LFLiveSession 实例
|
||
- (void)saveCurrentFrameToPhotoAlbum:(UIImage *)image {
|
||
if (!image) {
|
||
NSLog(@"当前没有图像可保存");
|
||
return;
|
||
}
|
||
|
||
// 检查权限
|
||
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
|
||
if (status == PHAuthorizationStatusDenied || status == PHAuthorizationStatusRestricted) {
|
||
NSLog(@"无权限访问相册,请到设置中开启权限");
|
||
return;
|
||
}
|
||
|
||
if (status == PHAuthorizationStatusNotDetermined) {
|
||
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus newStatus) {
|
||
if (newStatus == PHAuthorizationStatusAuthorized) {
|
||
[self saveImage:image];
|
||
}
|
||
}];
|
||
} else {
|
||
[self saveImage:image];
|
||
}
|
||
}
|
||
|
||
- (void)saveImage:(UIImage *)image {
|
||
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), NULL);
|
||
}
|
||
|
||
// 保存完成回调
|
||
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
|
||
if (error) {
|
||
NSLog(@"保存失败:%@", error.localizedDescription);
|
||
} else {
|
||
NSLog(@"已保存当前视频帧至相册");
|
||
}
|
||
}
|
||
|
||
- (void)dealloc {
|
||
// 禁止息屏
|
||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||
NSLog(@"###%@ dealloc",NSStringFromClass(self.class));
|
||
}
|
||
/*
|
||
#pragma mark - Navigation
|
||
|
||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||
// Get the new view controller using [segue destinationViewController].
|
||
// Pass the selected object to the new view controller.
|
||
}
|
||
*/
|
||
|
||
#pragma marks SellyCloudRTMPDelegate
|
||
- (void)pusher:(SellyVideoPusher *)pusher liveStatusDidChanged:(SellyLiveState)status {
|
||
|
||
}
|
||
|
||
- (void)pusher:(SellyVideoPusher *)pusher onStatisticsUpdate:(SellyLivePusherStats *)stats {
|
||
NSLog(@"##stats == %@",stats);
|
||
self.statsView.stats = stats;
|
||
}
|
||
|
||
- (void)pusher:(SellyVideoPusher *)pusher onError:(NSError *)error {
|
||
//推流报错,直播结束
|
||
NSLog(@"###onPusherError == %@",error);
|
||
[self.navigationController popViewControllerAnimated:true];
|
||
[self.view showToast:error.localizedDescription];
|
||
}
|
||
|
||
- (CVPixelBufferRef)pusher:(SellyVideoPusher *)pusher onCaptureVideoFrame:(CVPixelBufferRef)pixelBuffer {
|
||
CVPixelBufferRef afterBuffer = [FUManager.shareManager renderItemsToPixelBuffer:pixelBuffer];
|
||
return afterBuffer;
|
||
}
|
||
|
||
- (SCLiveItemContainerView *)itemContainer {
|
||
if (!_itemContainer) {
|
||
_itemContainer = [[SCLiveItemContainerView alloc] init];
|
||
}
|
||
return _itemContainer;
|
||
}
|
||
|
||
- (UIView *)liveView {
|
||
if (!_liveView) {
|
||
_liveView = UIView.new;
|
||
}
|
||
return _liveView;
|
||
}
|
||
|
||
- (SCLiveStatsView *)statsView {
|
||
if (!_statsView) {
|
||
_statsView = [[SCLiveStatsView alloc] init];
|
||
}
|
||
return _statsView;
|
||
}
|
||
@end
|