Feng's Notes

周海锋的个人博客

0%

年会直播考虑到公司出口带宽的问题,需要在内网搭建一个srs直播服务,记录下搭建过程

Linux 内核参数优化

修改 /etc/sysctl.conf

fs.file-max = 999999
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.ip_local_port_range = 1024 61000
net.ipv4.tcp_rmem = 4096 32768 262142
net.ipv4.tcp_wmem = 4096 32768 262142
net.core.netdev_max_backlog = 8096
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 2097152
net.core.wmem_max = 2097152
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn.backlog=1024

sysctl -p

ulimit -n 查看是否生效

安装常用的运维软件

# 编译软件
yum install -y gcc gcc-c++ g++ make jq libpam-cracklib openssl-devel bzip2-devel ffmpeg
# 常规软件
yum install -y nano vim git unzip wget ntpdate dos2unix net-tools
yum install -y tree htop ncdu nload sysstat psmisc bash-completion fail2ban chrony nfs-utils
# 清空缓存和已下载安装的软件包
yum clean all

编译 SRS

git clone -b develop https://gitee.com/ossrs/srs.git &&
cd srs/trunk && ./configure && make && ./objs/srs -c conf/srs.conf

服务操作

./etc/init.d/srs status
./etc/init.d/srs start
./etc/init.d/srs stop
./etc/init.d/srs reload

配置防火墙

firewall-cmd --permanent --add-port=1935/tcp
firewall-cmd --permanent --add-port=1985/tcp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=443/tcp
firewall-cmd --reload

拉流转推

ffmpeg -i rtmp://xxx/live/xxx -c:v libx264 -tune zerolatency -c:a copy -f flv rtmp://127.0.0.1:1935/live/xxx

碰到的问题

1、web播放提示跨域问题?

srs已经默认开启支持跨域,这个问题是因为 chrome 92以后,默认禁止http内网地址跨域访问资源。issue. 解决方案:配置下https证书。

新版本发布以后,发现SDK统计的版本号都变成了应用版本号,测试版本都是正确的,只有线上版本有问题。

最后发现是App端在使用 Xcode 13 提交 App Store 的时候,勾选了 Manage version and build number, 导致动态库中的版本号也被修改成应用版本号了。

gkcfm.png

一、问题说明

运营反馈近期客户端页面加载变慢,经常需要10~20s以上的时间才能显示出,甚至加载不出来,需要手动退出,重新打开才能出现。

二、问题分析

  1. 首先怀疑是DNS问题,用的是运营商的作为首选DNS,修改成114.114.114.114 没有效果。
  2. 通过输出请求连接返回时间,发现部分 https 资源访问极慢,切换到 http 地址,访问变快
  3. 初步怀疑是 ssl 证书引起的,测试发现,加载缓慢的https地址的证书都是 digicert 的,证书校验ocsp地址为 http://ocsp.digicert.com,通过chinaz 检测,目前这个地址国内访问很慢

image.png

  1. 通过替换加载链接:https://www.baidu.com/ (GlobalSign证书) https://suowo.cn/ (Digicert证书, oscp地址与我们相同) 验证。 百度加载很快,suowo.cn 的表现和我们一致,加载需要很久,基本确认就是证书的问题。

三、解决方案

  • 方案一: 目前我们使用的是在阿里云上申请的单域名免费证书,只有 Digicert 的国外验证地址可选。 考虑替换国内 ocsp 验证的收费证书。
  • 方案二:客户端临时替换成加载 http 页面,根据页面来切换请求的地址是 http/https。
  • 方案三:开启nginx 和 cdn 的 OCSP stapling, 由于国内cdn节点访问 ocsp 地址也很慢,经过验证,还是会间歇性访问超时

不久前,苹果WWDC19开发者大会在美国加州圣何塞举行。这次大会上,苹果公布了 iOS13 的更新内容,今天我们来看看如何适配 iOS13.

第三方登录

Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.

如果 APP 支持三方登陆(Facbook、Google、微信、QQ、支付宝等),就必须支持苹果登,且要放前边.

Sign in with Apple 设计规范

黑夜模式

Apps on iOS 13 are expected to support dark mode
Use system colors and materials
Create your own dynamic colors and images Leverage flexible infrastructure

审核强制要求适配黑夜模式。

KVC 限制

iOS13 以后已经不能肆无忌惮的通过 KVC 来修改一些没有暴露出来的属性了。

*** Terminating app due to uncaught exception ‘NSGenericException’, reason: ‘Access to xxx’s _xxx ivar is prohibited. This is an application bug’

已知:

// UITextField 的 _placeholderLabel
[textField setValue:[UIColor xxx] forKeyPath:@"_placeholderLabel.textColor"];

// UISearchBar 的 _searchField
[searchBar valueForKey:@"_searchField"];

模态弹出默认交互改变

/*
 Defines the presentation style that will be used for this view controller when it is presented modally. Set this property on the view controller to be presented, not the presenter.
 If this property has been set to UIModalPresentationAutomatic, reading it will always return a concrete presentation style. By default UIViewController resolves UIModalPresentationAutomatic to UIModalPresentationPageSheet, but other system-provided view controllers may resolve UIModalPresentationAutomatic to other concrete presentation styles.
 Defaults to UIModalPresentationAutomatic on iOS starting in iOS 13.0, and UIModalPresentationFullScreen on previous versions. Defaults to UIModalPresentationFullScreen on all other platforms.
 */
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle API_AVAILABLE(ios(3.2));

iOS 13presentViewController 默认有视差效果,模态出来的界面现在默认都下滑返回。
一些页面必须要点确认才能消失的,需要适配。如果项目中页面高度全部是屏幕尺寸,那么多出来的导航高度会出现问题。

// Swift
self.modalPresentationStyle = .fullScreen

// Objective-C
self.modalPresentationStyle = UIModalPresentationFullScreen;

UISegmentedControl 默认样式改变

默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改

App启动过程中,部分View可能无法实时获取到frame

可能是为了优化启动速度,App 启动过程中,部分View可能无法实时获取到正确的frame

// 只有等执行完 UIViewController 的 viewDidAppear 方法以后,才能获取到正确的值,在viewDidLoad等地方 frame Size 为 0,例如:
 [[UIApplication sharedApplication] statusBarFrame];

其他参考:

ChenYilong/iOS13AdaptationTips

Alibaba 阿里巴巴
Anjuke 安居客
Autohome 汽车之家
Bilibili 哔哩哔哩
Baidu 百度
Bytedance 字节跳动
Changba 唱吧

https://github.com/ChangbaDevs

Ctrip 携程
DiDi 滴滴出行
Dianwoda 点我达
Douban 豆瓣
Eleme 饿了么
Hellobike 哈喽单车
iqiyi 爱奇艺
ireader 掌阅
iuap 用友
JD 京东
Lianjia 链家
Meili 美丽联合(蘑菇街|美丽说)
Meituan 美团点评
Meitu 美图
MeetYou 美柚
Mobiker 摩拜
Netease 网易
Qunar 去哪儿
Sina 新浪
Souche 大搜车
TalkingData

https://github.com/TalkingData

Tencent 腾讯
Vip 唯品会
VipKid
Wacai 挖财
Weibo 微博
XiaoMi 小米
Youzan 有赞
YY 欢聚时代
Zhihu 知乎
奇虎360/360企业安全

GCDAsynSocket

GCDAsynSocket
是一个开源的基于GCD的异步的socket库。它支持IPV4和IPV6地址,SSL/TLS协议。同时它支持iOS端和Mac端。本篇主要介绍一下 GCDAsynSocket 中的 SSL/TLS 双向手动验证的实现

证书

  • p12证书:

    // 通过 pem 证书转换而来
    openssl pkcs12 -export -in my_cer.pem -out my_cer.p12
  • cer证书:

/*
// 方法一:
// 服务器自签名证书:
// openssl req -new -x509 -nodes -days 365 -newkey rsa:1024  -out my_cer.crt -keyout my_cer.key

// 需要cer格式证书,分发到终端后需要转换一下
// openssl x509 -outform der -in my_cer.crt -out my_cer.der

// 后缀名改成 cer 即可

//方法二:
// 1.获取根证书p12文件 
// 2.导入钥匙串
// 3.从钥匙串导出根证书cer文件即可 

*/

SSL/TLS

- (void)addSecurtyTransport {
    NSMutableDictionary *settings = [NSMutableDictionary dictionary];
    // SSL 证书
    NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"my_gateway" ofType:@"p12"]];
    CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(pkcs12data);
    // c语言字符串
    CFStringRef password = CFSTR("123456");

    const void *keys[] = { kSecImportExportPassphrase };
    const void *values[] = { password };

    CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);

    OSStatus securityError = SecPKCS12Import(inPKCS12Data, options, &items);
    CFRelease(options);
    CFRelease(password);

    if(securityError == errSecSuccess)
    ZQLog(@"Success opening p12 certificate.");

    CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
    SecIdentityRef myIdent = (SecIdentityRef)CFDictionaryGetValue(identityDict,
    kSecImportItemIdentity);
    SecIdentityRef certArray[1] = { myIdent };
    CFArrayRef myCerts = CFArrayCreate(NULL, (void *)certArray, 1, NULL);

    //开始手动SSL证书验证,必定要设置此key
    [settings setObject:(id)CFBridgingRelease(myCerts) forKey:(NSString *)kCFStreamSSLCertificates];
    [settings setObject:@(YES) forKey:GCDAsyncSocketManuallyEvaluateTrust];
    [settings setObject:@(NO) forKey:GCDAsyncSocketUseCFStreamForTLS];

    [self.socketClient startTLS:settings];
    [self.socketClient readDataWithTimeout:-1 tag:0];
}

#progma mark - Delegate

// # socket已连接,开启SSL
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    if (self.useTLS) {
        [self addSecurtyTransport];
    } 
}

// # SSL连接已建立
- (void)socketDidSecure:(GCDAsyncSocket *)sock {
    if (self.useTLS) {
        _socketClient = sock;
    }
}

// # 手动验证服务端证书
- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler {
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"my_gateway" ofType:@"cer"];
    NSData *cerData = [NSData dataWithContentsOfFile:cerPath];

    OSStatus status = -1;
    SecTrustResultType result = kSecTrustResultDeny;
    if (cerData) {
        SecCertificateRef cert1;
        // 将DER encoded X.509转换成 SecCertificateRef
        cert1 = SecCertificateCreateWithData(NULL, (__bridge CFDataRef) cerData);
        NSArray *caArray = [NSArray arrayWithObjects:(__bridge id)(cert1), nil];
        // 设置证书用于验证
        SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)caArray);
        // 同步验证服务器证书和本地证书是否匹配,会一直阻塞验证
        status = SecTrustEvaluate(trust, &result);
        CFRelease(cert1);
    } else {
        NSLog(@"local certificate could't be loaded!");
        completionHandler(NO);
    }

    if ((status == noErr &&
    (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified))) {
        NSLog(@"成功通过验证,证书可信");
        completionHandler(YES);
    } else {
        CFArrayRef arrayRefTrust = SecTrustCopyProperties(trust);
        NSLog(@"error in connection occured\n %@", arrayRefTrust);
        completionHandler(NO);
    }
}

自签名证书

// 自签名证书需要设置
[settings setObject:@(YES) forKey:GCDAsyncSocketUseCFStreamForTLS];

- (double)getCurrentBatteryLevel {
    UIApplication *app = [UIApplication sharedApplication];
    if (app.applicationState == UIApplicationStateActive || app.applicationState == UIApplicationStateInactive) {
        id statusBar;
        // 判断是否是iPhoneX, 兼容iOS12
        if ([[app valueForKeyPath:@"_statusBar"] isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")]) {
            statusBar = (UIView *)[[app valueForKeyPath:@"_statusBar"] valueForKeyPath:@"_statusBar"];
            NSDictionary *items = [statusBar valueForKeyPath:@"_items"];
            
            if (items && [items isKindOfClass:[NSDictionary class]]) {
                for (id item in [items allValues]) {
                    NSLog(@"%@", NSStringFromClass([item class]));
                    if ([NSStringFromClass([item class]) caseInsensitiveCompare:@"_UIStatusBarBatteryItem"] == NSOrderedSame) {
                        UIView *batteryView = [item valueForKeyPath:@"_batteryView"];
                        double batteryLevel = [[batteryView valueForKeyPath:@"_chargePercent"] doubleValue];
                        if (batteryLevel > 0 && batteryLevel <= 1) {
                            return batteryLevel;
                        }
                    }
                }
            }
        } else {
            statusBar = (UIView *)[app valueForKeyPath:@"_statusBar"];
            for (id aview in [statusBar subviews]) {
                int batteryLevel = 0;
                for (id bview in [aview subviews]) {
                    if ([NSStringFromClass([bview class]) caseInsensitiveCompare:@"UIStatusBarBatteryItemView"] == NSOrderedSame) {
                        Ivar ivar = class_getInstanceVariable([bview class],"_capacity");
                        if (ivar) {
                            batteryLevel = ((int (*)(id, Ivar))object_getIvar)(bview, ivar);
                            if (batteryLevel > 0 && batteryLevel <= 100) {
                                return batteryLevel / 100.0;
                            }
                        }
                    }
                }
            }
        }
    }
    return 0;
}

1. iOS 游戏直播方案简介

  • iOS 9 之前可以通过私有框架 CoreSurface.framework 来实现录制屏幕。因为用了私有框架,只能通过企业包的形式安装到用户设备中,这种方法的优点是效率很高,但是无法获取游戏声音,只能通过麦克风录制外放的声音。

  • iOS 9 以后,苹果去掉了 CoreSurface, 因此,上面的方法彻底失效。iOS 9 发布以后的很长一段时间,都没有办法录屏直播。 后来大家另辟蹊径,通过破解 AirPlay 投屏协议的方式实现,在手机上虚拟一个 AirPlay Server 来接受屏幕镜像, 然后解码后再直播。目前 大部分直播平台都是直接接入第三方 SDK,如乐播, xindawn。 这种方案的缺点是 每次 iOS 系统升级,对应的 Airplay Mirroring协议会更新,破解成本高,技术门槛比较高。

  • iOS 10 中苹果提供 ReplayKit 了,可以在游戏中实现录屏直播,但是需要游戏厂商支持通用性很低。所以基本上各大厂商基本上还是采用 Airplay的模式。

  • iOS 11 苹果增强为ReplayKit2 提供了更通用的桌面级录屏方案,本文接下来会着重介绍这种方案。

2. ReplayKit2 概述

录屏功能是 iOS 10 新推出的特性,苹果在 iOS 9 的 ReplayKit 保存录屏视频的基础上,增加了视频流实时直播功能,官方介绍见 Go Live with ReplayKit。iOS 11 增强为 ReplayKit2,进一步提升了 Replaykit 的易用性和通用性,并且可以对整个手机实现屏幕录制,而非某些做了支持ReplayKit功能的App,因此录屏推流建议直接使用iOS11的ReplayKit2屏幕录制方式。系统录屏采用的是扩展方式,扩展程序有单独的进程,iOS 系统为了保证系统流畅,给扩展程序的资源相对较少,扩展程序内存占用过大也会被 Kill 掉。

3. 部分关键功能实现

# 系统回调处理
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    KSYRKStreamerKit* kit = [KSYRKStreamerKit sharedInstance];
    switch (sampleBufferType) {
    case RPSampleBufferTypeVideo: {
        CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        pixelBuffer = [kit resizeAndRotatePixelBuffer:pixelBuffer];
        CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        if (sampleBuffer) {
            [kit.streamerBase processVideoPixelBuffer:pixelBuffer timeInfo:pts];
            _tempVideoPts = pts;
            _tempVideoPixelBuffer = pixelBuffer;
            _tempVideoTimeStamp = [[NSDate date] timeIntervalSince1970];
        }
    }
    break;
    case RPSampleBufferTypeAudioApp:
        [kit mixAudio:sampleBuffer to:kit.appTrack];
    break;
    case RPSampleBufferTypeAudioMic:
        [kit mixAudio:sampleBuffer to:kit.micTrack];
        kit.streamerBase.bWithAudio = YES;
        self.hasMicVoice = YES;
        break;
        default:
    break;
    }
}

4. 部分已知问题及解决方案

4.1 屏幕帧方向

系统回调回来的视频帧都是竖屏的全尺寸图像,我们需要对其进行处理

- (CVPixelBufferRef)resizeAndRotatePixelBuffer:(CVPixelBufferRef)sourcePixelBuffer {
    CIImage *outputImage;
    if (self.privacyMode) {
        outputImage = self.privacyImage;
    } else {
    //11.1以上支持自动旋转
        #ifdef __IPHONE_11_1
        if (UIDevice.currentDevice.systemVersion.floatValue > 11.1) {
            CGImagePropertyOrientation oritation = ((__bridge NSNumber*)CMGetAttachment(sampleBuffer, (__bridge CFStringRef)RPVideoSampleOrientationKey , NULL)).unsignedIntValue;
            if (oritation != self.lastOritation) {
                self.lastOritation = oritation;
                if (oritation == kCGImagePropertyOrientationLeft) {
                    [kit setVideoOrientation:UIDeviceOrientationLandscapeLeft];
                } else if (oritation == kCGImagePropertyOrientationRight) {
                    [kit setVideoOrientation:UIDeviceOrientationLandscapeRight];
                }
            }
        }
        #endif
        CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:sourcePixelBuffer];
        sourceImage = [sourceImage imageByApplyingOrientation:cvMobileRotate];
        CGFloat outputWidth  = self.videoSize.width;
        CGFloat outputHeight = self.videoSize.height;
        CGFloat inputWidth = sourceImage.extent.size.width;
        CGFloat inputHeight = sourceImage.extent.size.height;
        //    float scale = MIN(outputWidth/inputWidth, outputHeight/inputHeight);
        CGAffineTransform tranfrom = CGAffineTransformMakeScale(outputWidth/inputWidth, outputHeight/inputHeight);
        outputImage = [sourceImage imageByApplyingTransform:tranfrom];
    }
    if (!outputPixelBuffer) {
        //推流
        NSDictionary* pixelBufferOptions = @{
            (NSString*) kCVPixelBufferWidthKey : @(self.videoSize.width),
            (NSString*) kCVPixelBufferHeightKey : @(self.videoSize.height),
            (NSString*) kCVPixelBufferOpenGLESCompatibilityKey : @YES,
            (NSString*) kCVPixelBufferIOSurfacePropertiesKey : @{}
        };
        CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault, self.videoSize.width, self.videoSize.height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)pixelBufferOptions, &outputPixelBuffer);

        if (ret!= noErr) {
            NSLog(@"创建streamer buffer失败");
            outputPixelBuffer = nil;
            return outputPixelBuffer;
        }
    }
    if (cicontext) {
        [cicontext render:outputImage toCVPixelBuffer:outputPixelBuffer bounds:outputImage.extent colorSpace:CGColorSpaceCreateDeviceRGB()];
    }
    return outputPixelBuffer;
}

4.2 隐私模式的实现

在直播过程中比如要切换到QQ,或者输入密码等操作,不方便给观众看到,就需要用到隐私模式,用一张或多张图片来代替屏幕截屏。

# UIImage 图片转成 CIImage 然后可以调整大小和方向,直接通过 CIContext 渲染到 CVPixelBufferRef 中
UIImage *privacyImage = [UIImage imageNamed:privacyImageName];
CIImage *sourceImage = [[CIImage alloc] initWithImage:privacyImage];

4.3 某些情况下视频帧不回调

缓存上一个视频帧,根据推流的fps适当的补帧

4.4 弹幕和礼物信息的显示

弹幕和礼物信息的显示有两种方案:

1、是在主 App 中,建立 Socket 连接,收到消息后,创建本地通知,显示礼物和弹幕,大部分直播应用采用这种方式比较多,对原来弹幕系统的改造比较小。

2、类似企鹅电竞的做法,通过Apns 远程推送通知的方式,实现弹幕礼物通知。

4.5 后台保活

采用第一种弹幕就涉及到主App的后台保活问题:

常用的几种后台保活方式:VOIP,后台定位,播放空白声音。

考虑到耗电和上线审核的问题,我们目前采用的是利用background task 播放空白声音。

# 创建 background task
self.taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
    [[UIApplication sharedApplication] endBackgroundTask:weakSelf.taskIdentifier];
    weakSelf.taskIdentifier = UIBackgroundTaskInvalid;
}];

# 播放背景音乐
self.taskTimer = [NSTimer scheduledTimerWithTimeInterval:20.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 61.f) {
    //创建播放器
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setActive:YES error:nil];
    [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
    AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:weakSelf.musicUrl error:nil];
    [audioPlayer prepareToPlay];
    [audioPlayer play];
    [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
}
}];

4.6 部分游戏无声音

腾讯系的游戏,例如王者荣耀,刺激战场,在 先开游戏,再开直播的情况下,会出现主播无法听到游戏声音的问题。

原因是我们利用 AVAudioPlayer 后台保活,我们设置了 AVAudioSession 的 Option 为 AVAudioSessionCategoryOptionMixWithOthers 保证能和其他应用共用扬声器,但是这属性会导致 已经在播放的非Mix的声音被停止。

解决的方案只能是告知用户,先打开直播,再进入游戏,这样后播放的声音才不会有问题。

4.7 锁屏后录屏断开

这个问题没有很好的解决办法,只能在断开以后创建通知告知用户。

5. 参考

  1. 腾讯云文档-游戏录屏(ReplayKit)
  2. replaykit2 直播踩坑总结
  3. iOS11 ReplayKit2 问题总结

扩展 FLV 标准支持 H265

Adobe 的FLV 标准里面并不支持 H.265(HEVC)。
要用 FLV 封装 H.265 数据 ,就需要扩展 FLV 增加 CodecID 来支持 H.265 。

目前国内CDN厂商扩展的CodecID一般都为12,具体的FLV H265标准信息,可以参考 金山的video_file_format_spec_v10_1

FFmpeg patch 方法可以参考:金山云H.265 patch说明

当前主流H265解码方案性能对比

  • 测试设备: iPhone X, iOS 11.4
  • 测试视频: raw h265文件, 1280 x 720, 3600帧, 单线程解码
方案 cost fps
ittiam CPU 11313ms 316.27
安卓自带hevc解码方案libhevc Real 11.311s 316.32
yyffmpeg CPU 13532ms 264.32
yy的h265解码方案 Real 13.449s 265.96
ffmpeg-3.4 CPU 53935ms 66.32
未优化版本 Real 53.432s 66.94
ffmpeg-3.4 CPU 13843ms 258.47
aarch64 neon优化,idct,qpel,pel等方法 Real 13.839s 258.54
ksc265 CPU 6749ms 544.19
金山H265 Real 6.239s 508.94
videotoolbox CPU 3749ms 955.14
iOS11 硬件解码 Real 17.139s 207.32

开源方案

ittiam的解码方案解码效率最高

iPhone 硬解需要 iPhone 6s 和 iOS11 以上的设备,cpu使用率很低,但是解码速度并不如一些软解方案快。

ffmpeg里的openhevc没有针对aarm64平台进行优化,解码效率最低

经过优化以后基本与yy的方案效率相近

商业非开源

ksc265 金山的解码方案的解码效率最高,sdk需要授权,同时cpu使用率也很高。

参考

  1. Minmin.Sun Blog “分析 H.265 + AAC 的 FLV 文件”
  2. video_file_format_spec_v10_1
  3. HEVCIOSDecoderDemoProj
  4. [FFmpeg-devel] Add ARM64 NEON optimization for HEVC decoder
  5. [FFmpeg-devel] [PATCH] 8-bit hevc decoding optimization on aarch64 with neon

FFmpeg 常用命令

1、手动编译FFmpeg

# 安装依赖
brew install automake fdk-aac lame libass libtool libvorbis libvpx opus sdl shtool texi2html theora wget x264 x265 xvid yasm  

# configure
./configure  --prefix=/usr/local --enable-gpl --enable-nonfree --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-ffplay --enable-libxvid --extra-ldflags=-L/usr/local/lib 

# 编译
 make && make install

2、通过 brew 安装 FFmpeg

# Mac 上安装FFmpeg, 包含ffplay和x265支持
brew install ffmpeg --with-fdk-aac --with-sdl2 --with-freetype --with-libass --with-libquvi --with-libvorbis --with-libvpx --with-opus --with-x265 --with-x264

# 如果已经安装,可以通过下面命令重新安装
brew reinstall ffmpeg --with-xxx

3、FFmpeg常用命令

H264转H265 命令

ffmpeg -i input.file -c:a copy -c:v libx265 output.file

H264 RTMP/Flv流 转 H265 RTMP/Flv

#支持H265推流的NodeJs服务器
npm install node-media-server

node run app.js
# 在线流转H265 RTMP流
ffmpeg -re -i [INPUT] -c:v libx265 -tune zerolatency -c:a copy -f flv rtmp://127.0.0.1:1935/live/h265