mirror of
https://github.com/DKJone/DKWechatHelper.git
synced 2026-07-29 06:12:07 +08:00
[v1.0.7](https://github.com/DKWechatHelper/DKWechatHelper/releases/tag/1.0.7) / 2021-01-29
what's new * 动态启动图 * 动态聊天背景 * 支持8.0.1 * 更新越狱包8.0.1 * 更新已注入助手的8.0.1未签名包 * 更新越狱源安装包
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
// QGBaseDecoder.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "QGAnimatedImageDecodeThread.h"
|
||||
#import "QGBaseDFileInfo.h"
|
||||
|
||||
@interface QGBaseDecoder : NSObject
|
||||
|
||||
@property (atomic, assign) NSInteger currentDecodeFrame; //正在解码的帧索引
|
||||
@property (nonatomic, readonly) QGBaseDFileInfo *fileInfo; //解码文件信息 只能通过初始化方法赋值
|
||||
|
||||
- (instancetype)initWith:(QGBaseDFileInfo *)fileInfo error:(NSError **)error;
|
||||
- (void)decodeFrame:(NSInteger)frameIndex buffers:(NSMutableArray *)buffers;
|
||||
- (BOOL)shouldStopDecode:(NSInteger)nextFrameIndex;
|
||||
- (BOOL)isFrameIndexBeyondEnd:(NSInteger)frameIndex;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,70 @@
|
||||
// QGBaseDecoder.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGBaseDecoder.h"
|
||||
#import "QGAnimatedImageDecodeThreadPool.h"
|
||||
|
||||
@interface QGBaseDecoder() {
|
||||
|
||||
QGBaseDFileInfo *_fileInfo;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation QGBaseDecoder
|
||||
|
||||
- (instancetype)initWith:(QGBaseDFileInfo *)fileInfo error:(NSError **)error {
|
||||
|
||||
if (self = [super init]) {
|
||||
_currentDecodeFrame = -1;
|
||||
_fileInfo = fileInfo;
|
||||
_fileInfo.occupiedCount ++;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (QGBaseDFileInfo *)fileInfo {
|
||||
|
||||
return _fileInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
由具体子类实现
|
||||
该方法在decodeframe方法即将被调用时调用,如果返回YES则停止解码工作
|
||||
|
||||
@param nextFrameIndex 将要解码的帧索引
|
||||
@return 是否需要继续解码
|
||||
*/
|
||||
- (BOOL)shouldStopDecode:(NSInteger)nextFrameIndex {
|
||||
// No implementation here. Meant to be overriden in subclass.
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isFrameIndexBeyondEnd:(NSInteger)frameIndex {
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
/**
|
||||
在专用线程内解码指定帧并放入对应的缓冲区内
|
||||
|
||||
@param frameIndex 帧索引
|
||||
@param buffers 缓冲
|
||||
*/
|
||||
- (void)decodeFrame:(NSInteger)frameIndex buffers:(NSMutableArray *)buffers {
|
||||
// No implementation here. Meant to be overriden in subclass.
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
// QGMP4FrameHWDecoder.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGBaseDecoder.h"
|
||||
#import "QGMP4HWDFileInfo.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/* 数字跳动的动画类型*/
|
||||
typedef NS_ENUM(NSInteger, QGMP4HWDErrorCode){
|
||||
|
||||
QGMP4HWDErrorCode_FileNotExist = 10000, // 文件不存在
|
||||
QGMP4HWDErrorCode_InvalidMP4File = 10001, // 非法的mp4文件
|
||||
QGMP4HWDErrorCode_CanNotGetStreamInfo = 10002, // 无法获取视频流信息
|
||||
QGMP4HWDErrorCode_CanNotGetStream = 10003, // 无法获取视频流
|
||||
QGMP4HWDErrorCode_ErrorCreateVTBDesc = 10004, // 创建desc失败
|
||||
QGMP4HWDErrorCode_ErrorCreateVTBSession = 10005, // 创建session失败
|
||||
};
|
||||
|
||||
@interface UIDevice (HWD)
|
||||
|
||||
- (BOOL)hwd_isSimulator;
|
||||
|
||||
@end
|
||||
|
||||
@interface QGMP4FrameHWDecoder : QGBaseDecoder
|
||||
|
||||
|
||||
+ (NSString *)errorDescriptionForCode:(QGMP4HWDErrorCode)errorCode;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,483 @@
|
||||
// QGMP4FrameHWDecoder.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGMP4FrameHWDecoder.h"
|
||||
#import "QGVAPWeakProxy.h"
|
||||
#import "QGMP4AnimatedImageFrame.h"
|
||||
#import "QGBaseAnimatedImageFrame+Displaying.h"
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import "QGHWDMP4OpenGLView.h"
|
||||
#import "QGMP4Parser.h"
|
||||
#import "QGVAPSafeMutableArray.h"
|
||||
#import "NSNotificationCenter+VAPThreadSafe.h"
|
||||
#include <sys/sysctl.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation UIDevice (HWD)
|
||||
|
||||
- (BOOL)hwd_isSimulator {
|
||||
static dispatch_once_t token;
|
||||
static BOOL isSimulator = NO;
|
||||
dispatch_once(&token, ^{
|
||||
NSString *model = [self machineName];
|
||||
if ([model isEqualToString:@"x86_64"] || [model isEqualToString:@"i386"]) {
|
||||
isSimulator = YES;
|
||||
}
|
||||
});
|
||||
return isSimulator;
|
||||
}
|
||||
|
||||
- (NSString *)machineName {
|
||||
static dispatch_once_t token;
|
||||
static NSString *name;
|
||||
dispatch_once(&token, ^{
|
||||
size_t size;
|
||||
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
|
||||
char *machineName = malloc(size);
|
||||
sysctlbyname("hw.machine", machineName, &size, NULL, 0);
|
||||
name = [NSString stringWithUTF8String:machineName];
|
||||
free(machineName);
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface NSArray (SafeOperation)
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSArray (SafeOperation)
|
||||
|
||||
- (id)safeObjectAtIndex:(NSUInteger)index
|
||||
{
|
||||
if (index >= self.count) {
|
||||
NSAssert(0, @"Error: access to array index which is beyond bounds! ");
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self[index];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface QGMP4FrameHWDecoder() {
|
||||
|
||||
NSMutableArray *_buffers;
|
||||
|
||||
int _videoStream;
|
||||
int _outputWidth, _outputHeight;
|
||||
OSStatus _status;
|
||||
BOOL _isFinish;
|
||||
VTDecompressionSessionRef _mDecodeSession;
|
||||
CMFormatDescriptionRef _mFormatDescription;
|
||||
NSInteger _finishFrameIndex;
|
||||
NSError *_constructErr;
|
||||
QGMP4ParserProxy *_mp4Parser;
|
||||
}
|
||||
|
||||
@property (atomic, strong) dispatch_queue_t decodeQueue; //dispatch decode task
|
||||
@property (nonatomic, strong) NSData *ppsData; //Picture Parameter Set
|
||||
@property (nonatomic, strong) NSData *spsData; //Sequence Parameter Set
|
||||
/** Video Parameter Set */
|
||||
@property (nonatomic, strong) NSData *vpsData;
|
||||
|
||||
@end
|
||||
|
||||
NSString *const QGMP4HWDErrorDomain = @"QGMP4HWDErrorDomain";
|
||||
|
||||
@implementation QGMP4FrameHWDecoder
|
||||
|
||||
+ (NSString *)errorDescriptionForCode:(QGMP4HWDErrorCode)errorCode {
|
||||
|
||||
NSArray *errorDescs = @[@"文件不存在",@"非法文件格式",@"无法获取视频流信息",@"无法获取视频流",@"VTB创建desc失败",@"VTB创建session失败"];
|
||||
NSString *desc = @"";
|
||||
switch (errorCode) {
|
||||
case QGMP4HWDErrorCode_FileNotExist:
|
||||
desc = [errorDescs safeObjectAtIndex:0];
|
||||
break;
|
||||
case QGMP4HWDErrorCode_InvalidMP4File:
|
||||
desc = [errorDescs safeObjectAtIndex:1];
|
||||
break;
|
||||
case QGMP4HWDErrorCode_CanNotGetStreamInfo:
|
||||
desc = [errorDescs safeObjectAtIndex:2];
|
||||
break;
|
||||
case QGMP4HWDErrorCode_CanNotGetStream:
|
||||
desc = [errorDescs safeObjectAtIndex:3];
|
||||
break;
|
||||
case QGMP4HWDErrorCode_ErrorCreateVTBDesc:
|
||||
desc = [errorDescs safeObjectAtIndex:4];
|
||||
break;
|
||||
case QGMP4HWDErrorCode_ErrorCreateVTBSession:
|
||||
desc = [errorDescs safeObjectAtIndex:5];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
- (instancetype)initWith:(QGMP4HWDFileInfo *)fileInfo error:(NSError *__autoreleasing *)error{
|
||||
|
||||
if (self = [super initWith:fileInfo error:error]) {
|
||||
_decodeQueue = dispatch_queue_create("com.qgame.vap.decode", DISPATCH_QUEUE_SERIAL);
|
||||
_mp4Parser = fileInfo.mp4Parser;
|
||||
BOOL isOpenSuccess = [self onInputStart];
|
||||
if (!isOpenSuccess) {
|
||||
VAP_Event(kQGVAPModuleCommon, @"onInputStart fail!");
|
||||
*error = _constructErr;
|
||||
self = nil;
|
||||
return nil;
|
||||
}
|
||||
[self registerNotification];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)registerNotification {
|
||||
|
||||
[[NSNotificationCenter defaultCenter] hwd_addSafeObserver:self selector:@selector(hwd_didReceiveEnterBackgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] hwd_addSafeObserver:self selector:@selector(hwd_didReceiveEnterBackgroundNotification:) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)hwd_didReceiveEnterBackgroundNotification:(NSNotification *)notification {
|
||||
|
||||
[self onInputEnd];
|
||||
}
|
||||
|
||||
- (void)decodeFrame:(NSInteger)frameIndex buffers:(NSMutableArray *)buffers {
|
||||
|
||||
if (frameIndex == self.currentDecodeFrame) {
|
||||
VAP_Event(kQGVAPModuleCommon, @"already in decode");
|
||||
return ;
|
||||
}
|
||||
self.currentDecodeFrame = frameIndex;
|
||||
_buffers = buffers;
|
||||
dispatch_async(self.decodeQueue, ^{
|
||||
[self _decodeFrame:frameIndex];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_decodeFrame:(NSInteger)frameIndex {
|
||||
|
||||
if (_isFinish) {
|
||||
return ;
|
||||
}
|
||||
|
||||
if (!_buffers) {
|
||||
return ;
|
||||
}
|
||||
|
||||
if (self.spsData == nil || self.ppsData == nil) {
|
||||
return ;
|
||||
}
|
||||
|
||||
//解码开始时间
|
||||
NSDate *startDate = [NSDate date];
|
||||
NSData *packetData = [_mp4Parser readPacketOfSample:frameIndex];
|
||||
if (!packetData.length) {
|
||||
_finishFrameIndex = frameIndex;
|
||||
[self onInputEnd];
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前帧pts,pts是在parse mp4 box时得到的
|
||||
uint64_t currentPts = [_mp4Parser.videoSamples[frameIndex] pts];
|
||||
|
||||
CVPixelBufferRef outputPixelBuffer = NULL;
|
||||
// 4. get NALUnit payload into a CMBlockBuffer,
|
||||
CMBlockBufferRef blockBuffer = NULL;
|
||||
|
||||
_status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
|
||||
(void *)packetData.bytes,
|
||||
packetData.length,
|
||||
kCFAllocatorNull, NULL, 0,
|
||||
packetData.length, 0,
|
||||
&blockBuffer);
|
||||
// 6. create a CMSampleBuffer.
|
||||
CMSampleBufferRef sampleBuffer = NULL;
|
||||
const size_t sampleSizeArray[] = {packetData.length};
|
||||
_status = CMSampleBufferCreateReady(kCFAllocatorDefault,
|
||||
blockBuffer,
|
||||
_mFormatDescription,
|
||||
1, 0, NULL, 1, sampleSizeArray,
|
||||
&sampleBuffer);
|
||||
|
||||
if (blockBuffer) {
|
||||
CFRelease(blockBuffer);
|
||||
}
|
||||
// 7. use VTDecompressionSessionDecodeFrame
|
||||
if (@available(iOS 9.0, *)) {
|
||||
__typeof(self) __weak weakSelf = self;
|
||||
VTDecodeFrameFlags flags = 0;
|
||||
VTDecodeInfoFlags flagOut = 0;
|
||||
VTDecompressionSessionDecodeFrameWithOutputHandler(_mDecodeSession, sampleBuffer, flags, &flagOut, ^(OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef _Nullable imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration) {
|
||||
CFRelease(sampleBuffer);
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(status == kVTInvalidSessionErr) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"decompress fail! frame:%@ kVTInvalidSessionErr error:%@", @(frameIndex), @(status));
|
||||
} else if(status == kVTVideoDecoderBadDataErr) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"decompress fail! frame:%@ kVTVideoDecoderBadDataErr error:%@", @(frameIndex), @(status));
|
||||
} else if(status != noErr) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"decompress fail! frame:%@ error:%@", @(frameIndex), @(status));
|
||||
}
|
||||
|
||||
QGMP4AnimatedImageFrame *newFrame = [[QGMP4AnimatedImageFrame alloc] init];
|
||||
// imagebuffer会在frame回收时释放
|
||||
CVPixelBufferRetain(imageBuffer);
|
||||
newFrame.pixelBuffer = imageBuffer;
|
||||
newFrame.frameIndex = frameIndex; //dts顺序
|
||||
NSTimeInterval decodeTime = [[NSDate date] timeIntervalSinceDate:startDate]*1000;
|
||||
newFrame.decodeTime = decodeTime;
|
||||
newFrame.defaultFps =(int) strongSelf->_mp4Parser.fps;
|
||||
newFrame.pts = currentPts;
|
||||
|
||||
// 8. insert into buffer
|
||||
[strongSelf->_buffers addObject:newFrame];
|
||||
|
||||
// 9. sort
|
||||
[strongSelf->_buffers sortUsingComparator:^NSComparisonResult(QGMP4AnimatedImageFrame * _Nonnull obj1, QGMP4AnimatedImageFrame * _Nonnull obj2) {
|
||||
return [@(obj1.pts) compare:@(obj2.pts)];
|
||||
}];
|
||||
});
|
||||
} else {
|
||||
// 7. use VTDecompressionSessionDecodeFrame
|
||||
VTDecodeFrameFlags flags = 0;
|
||||
VTDecodeInfoFlags flagOut = 0;
|
||||
_status = VTDecompressionSessionDecodeFrame(_mDecodeSession, sampleBuffer, flags, &outputPixelBuffer, &flagOut);
|
||||
|
||||
if(_status == kVTInvalidSessionErr) {
|
||||
} else if(_status == kVTVideoDecoderBadDataErr) {
|
||||
} else if(_status != noErr) {
|
||||
}
|
||||
CFRelease(sampleBuffer);
|
||||
|
||||
QGMP4AnimatedImageFrame *newFrame = [[QGMP4AnimatedImageFrame alloc] init];
|
||||
// imagebuffer会在frame回收时释放
|
||||
newFrame.pixelBuffer = outputPixelBuffer;
|
||||
newFrame.frameIndex = frameIndex;
|
||||
NSTimeInterval decodeTime = [[NSDate date] timeIntervalSinceDate:startDate]*1000;
|
||||
newFrame.decodeTime = decodeTime;
|
||||
newFrame.defaultFps = (int)_mp4Parser.fps;
|
||||
|
||||
// 8. insert into buffer
|
||||
[_buffers addObject:newFrame];
|
||||
|
||||
// 9. sort
|
||||
[_buffers sortUsingComparator:^NSComparisonResult(QGMP4AnimatedImageFrame * _Nonnull obj1, QGMP4AnimatedImageFrame * _Nonnull obj2) {
|
||||
return [@(obj1.pts) compare:@(obj2.pts)];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - override
|
||||
|
||||
- (BOOL)shouldStopDecode:(NSInteger)nextFrameIndex {
|
||||
return _isFinish;
|
||||
}
|
||||
|
||||
- (BOOL)isFrameIndexBeyondEnd:(NSInteger)frameIndex {
|
||||
|
||||
if (_finishFrameIndex > 0) {
|
||||
return (frameIndex >= _finishFrameIndex);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
[self _onInputEnd];
|
||||
self.fileInfo.occupiedCount --;
|
||||
if (self.fileInfo.occupiedCount <= 0) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - private methods
|
||||
|
||||
- (BOOL)onInputStart {
|
||||
|
||||
NSFileManager *fileMgr = [NSFileManager defaultManager];
|
||||
if (![fileMgr fileExistsAtPath:self.fileInfo.filePath]) {
|
||||
_constructErr = [NSError errorWithDomain:QGMP4HWDErrorDomain code:QGMP4HWDErrorCode_FileNotExist userInfo:[self errorUserInfo]];
|
||||
return NO;
|
||||
}
|
||||
|
||||
_isFinish = NO;
|
||||
self.vpsData = nil;
|
||||
self.spsData = nil;
|
||||
self.ppsData = nil;
|
||||
_outputWidth = (int)_mp4Parser.picWidth;
|
||||
_outputHeight = (int)_mp4Parser.picHeight;
|
||||
|
||||
BOOL paramsSetInitSuccess = [self initPPSnSPS];
|
||||
return paramsSetInitSuccess;
|
||||
}
|
||||
|
||||
- (BOOL)initPPSnSPS {
|
||||
|
||||
VAP_Info(kQGVAPModuleCommon, @"initPPSnSPS");
|
||||
if (self.spsData && self.ppsData) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"sps&pps is already has value.");
|
||||
return YES;
|
||||
}
|
||||
|
||||
self.spsData = _mp4Parser.spsData;
|
||||
self.ppsData = _mp4Parser.ppsData;
|
||||
self.vpsData = _mp4Parser.vpsData;
|
||||
|
||||
// 2. create CMFormatDescription
|
||||
if (self.spsData != nil && self.ppsData != nil && _mp4Parser.videoCodecID != QGMP4VideoStreamCodecIDUnknown) {
|
||||
if (_mp4Parser.videoCodecID == QGMP4VideoStreamCodecIDH264) {
|
||||
const uint8_t* const parameterSetPointers[2] = { (const uint8_t*)[self.spsData bytes], (const uint8_t*)[self.ppsData bytes] };
|
||||
const size_t parameterSetSizes[2] = { [self.spsData length], [self.ppsData length] };
|
||||
|
||||
_status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
||||
2,
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
4,
|
||||
&_mFormatDescription);
|
||||
if (_status != noErr) {
|
||||
VAP_Event(kQGVAPModuleCommon, @"CMVideoFormatDescription. Creation: %@.", (_status == noErr) ? @"successfully." : @"failed.");
|
||||
_constructErr = [NSError errorWithDomain:QGMP4HWDErrorDomain code:QGMP4HWDErrorCode_ErrorCreateVTBDesc userInfo:[self errorUserInfo]];
|
||||
return NO;
|
||||
}
|
||||
} else if (_mp4Parser.videoCodecID == QGMP4VideoStreamCodecIDH265) {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if(VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
|
||||
const uint8_t* const parameterSetPointers[3] = {(const uint8_t*)[self.vpsData bytes], (const uint8_t*)[self.spsData bytes], (const uint8_t*)[self.ppsData bytes]};
|
||||
const size_t parameterSetSizes[3] = {[self.vpsData length], [self.spsData length], [self.ppsData length]};
|
||||
|
||||
_status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
||||
3, // parameter_set_count
|
||||
parameterSetPointers, // ¶meter_set_pointers
|
||||
parameterSetSizes, // ¶meter_set_sizes
|
||||
4, // nal_unit_header_length
|
||||
NULL,
|
||||
&_mFormatDescription);
|
||||
if (_status != noErr) {
|
||||
VAP_Event(kQGVAPModuleCommon, @"CMVideoFormatDescription. Creation: %@.", (_status == noErr) ? @"successfully." : @"failed.");
|
||||
_constructErr = [NSError errorWithDomain:QGMP4HWDErrorDomain code:QGMP4HWDErrorCode_ErrorCreateVTBDesc userInfo:[self errorUserInfo]];
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
VAP_Event(kQGVAPModuleCommon, @"H.265 decoding is un-supported because of the hardware");
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
VAP_Event(kQGVAPModuleCommon, @"System version is too low to support H.265 decoding");
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. create VTDecompressionSession
|
||||
CFDictionaryRef attrs = NULL;
|
||||
const void *keys[] = {kCVPixelBufferPixelFormatTypeKey};
|
||||
// kCVPixelFormatType_420YpCbCr8Planar is YUV420
|
||||
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange is NV12
|
||||
uint32_t v = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
|
||||
const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &v) };
|
||||
attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
|
||||
|
||||
if ([UIDevice currentDevice].systemVersion.floatValue >= 9.0) {
|
||||
_status = VTDecompressionSessionCreate(kCFAllocatorDefault,
|
||||
_mFormatDescription,
|
||||
NULL,
|
||||
attrs,
|
||||
NULL,
|
||||
&_mDecodeSession);
|
||||
if (_status != noErr) {
|
||||
CFRelease(attrs);
|
||||
_constructErr = [NSError errorWithDomain:QGMP4HWDErrorDomain code:QGMP4HWDErrorCode_ErrorCreateVTBSession userInfo:[self errorUserInfo]];
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
VTDecompressionOutputCallbackRecord callBackRecord;
|
||||
callBackRecord.decompressionOutputCallback = didDecompress;
|
||||
callBackRecord.decompressionOutputRefCon = NULL;
|
||||
|
||||
_status = VTDecompressionSessionCreate(kCFAllocatorDefault,
|
||||
_mFormatDescription,
|
||||
NULL, attrs,
|
||||
&callBackRecord,
|
||||
&_mDecodeSession);
|
||||
if (_status != noErr) {
|
||||
CFRelease(attrs);
|
||||
_constructErr = [NSError errorWithDomain:QGMP4HWDErrorDomain code:QGMP4HWDErrorCode_ErrorCreateVTBSession userInfo:[self errorUserInfo]];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
CFRelease(attrs);
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)_onInputEnd {
|
||||
if (_isFinish) {
|
||||
return ;
|
||||
}
|
||||
_isFinish = YES;
|
||||
if (_mDecodeSession) {
|
||||
VTDecompressionSessionWaitForAsynchronousFrames(_mDecodeSession);
|
||||
VTDecompressionSessionInvalidate(_mDecodeSession);
|
||||
CFRelease(_mDecodeSession);
|
||||
_mDecodeSession = NULL;
|
||||
}
|
||||
if (self.spsData || self.ppsData || self.vpsData) {
|
||||
self.spsData = nil;
|
||||
self.ppsData = nil;
|
||||
self.vpsData = nil;
|
||||
}
|
||||
if (_mFormatDescription) {
|
||||
CFRelease(_mFormatDescription);
|
||||
_mFormatDescription = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onInputEnd {
|
||||
|
||||
//为确保任务停止,必须同步执行
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
if ([NSThread isMainThread]) {
|
||||
dispatch_sync(self.decodeQueue, ^{
|
||||
[weakSelf _onInputEnd];
|
||||
});
|
||||
} else {
|
||||
dispatch_async(self.decodeQueue, ^{
|
||||
[weakSelf _onInputEnd];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//decode callback
|
||||
void didDecompress(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ){
|
||||
|
||||
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
|
||||
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
|
||||
}
|
||||
|
||||
- (NSDictionary *)errorUserInfo {
|
||||
NSDictionary *userInfo = @{@"location" : self.fileInfo.filePath ? : @""};
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
// QGAnimatedImageBufferManager.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "QGAnimatedImageDecodeConfig.h"
|
||||
#import "QGBaseAnimatedImageFrame.h"
|
||||
|
||||
@interface QGAnimatedImageBufferManager : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *buffers;//缓冲
|
||||
|
||||
- (instancetype)initWithConfig:(QGAnimatedImageDecodeConfig *)config;
|
||||
- (QGBaseAnimatedImageFrame *)getBufferedFrame:(NSInteger)frameIndex;
|
||||
- (BOOL)isBufferFull;
|
||||
- (QGBaseAnimatedImageFrame *)popVideoFrame;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,101 @@
|
||||
// QGAnimatedImageBufferManager.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGAnimatedImageBufferManager.h"
|
||||
#import "QGVAPSafeMutableArray.h"
|
||||
|
||||
@interface QGAnimatedImageBufferManager() {
|
||||
|
||||
QGAnimatedImageDecodeConfig *_config; //解码配置
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation QGAnimatedImageBufferManager
|
||||
|
||||
- (instancetype)initWithConfig:(QGAnimatedImageDecodeConfig *)config {
|
||||
|
||||
if (self = [super init]) {
|
||||
_config = config;
|
||||
[self createBuffersWithConfig:config];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)createBuffersWithConfig:(QGAnimatedImageDecodeConfig *)config {
|
||||
_buffers = [[QGVAPSafeMutableArray alloc] initWithCapacity:config.bufferCount];
|
||||
}
|
||||
|
||||
/**
|
||||
取出指定的在缓冲区的帧,若不存在于缓冲区则返回空
|
||||
|
||||
@param frameIndex 目标帧索引
|
||||
@return 帧数据
|
||||
*/
|
||||
- (QGBaseAnimatedImageFrame *)getBufferedFrame:(NSInteger)frameIndex {
|
||||
|
||||
if (_buffers.count == 0) {
|
||||
//NSLog(@"fail buffer is nil");
|
||||
return nil;
|
||||
}
|
||||
NSInteger bufferIndex = frameIndex%_buffers.count;
|
||||
if (bufferIndex > _buffers.count-1) {
|
||||
//NSLog(@"fail");
|
||||
return nil;
|
||||
}
|
||||
id frame = [_buffers objectAtIndex:bufferIndex];
|
||||
if (![frame isKindOfClass:[QGBaseAnimatedImageFrame class]] || ([(QGBaseAnimatedImageFrame*)frame frameIndex] != frameIndex)) {
|
||||
return nil;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (QGBaseAnimatedImageFrame *)popVideoFrame {
|
||||
if (!_buffers.count) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![_buffers.firstObject isKindOfClass:[QGBaseAnimatedImageFrame class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
QGBaseAnimatedImageFrame *frame = _buffers.firstObject;
|
||||
[_buffers removeObjectAtIndex:0];
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
判断当前缓冲区是否被填满
|
||||
|
||||
@return 只有当缓冲区所有区域都被QGBaseAnimatedImageFrame类型的数据填满才算缓冲区满
|
||||
*/
|
||||
- (BOOL)isBufferFull {
|
||||
|
||||
__block BOOL isFull = YES;
|
||||
[_buffers enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if (![obj isKindOfClass:[QGBaseAnimatedImageFrame class]]) {
|
||||
isFull = NO;
|
||||
*stop = YES;
|
||||
}
|
||||
}];
|
||||
return isFull;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,25 @@
|
||||
// QGAnimatedImageDecodeConfig.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface QGAnimatedImageDecodeConfig : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSInteger threadCount;//线程数
|
||||
@property (nonatomic, assign) NSInteger bufferCount;//缓冲数
|
||||
|
||||
+ (instancetype)defaultConfig;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
// QGAnimatedImageDecodeConfig.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGAnimatedImageDecodeConfig.h"
|
||||
|
||||
@implementation QGAnimatedImageDecodeConfig
|
||||
|
||||
+ (instancetype)defaultConfig {
|
||||
|
||||
QGAnimatedImageDecodeConfig *config = [QGAnimatedImageDecodeConfig new];
|
||||
config.threadCount= 1;
|
||||
config.bufferCount = 5;
|
||||
return config;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,57 @@
|
||||
// QGAnimatedImageDecodeManager.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "QGBaseDecoder.h"
|
||||
#import "QGBaseAnimatedImageFrame.h"
|
||||
#import "QGAnimatedImageDecodeConfig.h"
|
||||
|
||||
@class QGAnimatedImageDecodeManager;
|
||||
@protocol QGAnimatedImageDecoderDelegate <NSObject>
|
||||
|
||||
/**
|
||||
必须实现该方法 用以实例化解码器
|
||||
|
||||
@param manager 解码控制器
|
||||
@return class
|
||||
*/
|
||||
- (Class)decoderClassForManager:(QGAnimatedImageDecodeManager *)manager;
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
到文件末尾时被调用
|
||||
|
||||
@param decoder 解码器
|
||||
*/
|
||||
- (void)decoderDidFinishDecode:(QGBaseDecoder *)decoder;
|
||||
|
||||
- (void)decoderDidFailDecode:(QGBaseDecoder *)decoder error:(NSError *)error;
|
||||
|
||||
@end
|
||||
|
||||
@interface QGAnimatedImageDecodeManager : NSObject
|
||||
|
||||
@property (nonatomic, weak) id<QGAnimatedImageDecoderDelegate> decoderDelegate;
|
||||
|
||||
- (instancetype)initWith:(QGBaseDFileInfo *)fileInfo
|
||||
config:(QGAnimatedImageDecodeConfig *)config
|
||||
delegate:(id<QGAnimatedImageDecoderDelegate>)delegate;
|
||||
|
||||
- (QGBaseAnimatedImageFrame *)consumeDecodedFrame:(NSInteger)frameIndex;
|
||||
|
||||
- (void)tryToStartAudioPlay;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,161 @@
|
||||
// QGAnimatedImageDecodeManager.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGAnimatedImageDecodeManager.h"
|
||||
#import "QGAnimatedImageBufferManager.h"
|
||||
#import "QGBaseDecoder.h"
|
||||
#import "QGVAPSafeMutableArray.h"
|
||||
#import "QGMP4FrameHWDecoder.h"
|
||||
#import "QGVAPLogger.h"
|
||||
#import <sys/stat.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface QGAnimatedImageDecodeManager() {
|
||||
|
||||
QGAnimatedImageDecodeConfig *_config; //解码配置
|
||||
QGBaseDFileInfo *_fileInfo; //sharpP文件信息
|
||||
NSMutableArray *_decoders; //解码器
|
||||
QGAnimatedImageBufferManager *_bufferManager; //缓冲管理
|
||||
AVAudioPlayer *_audioPlayer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation QGAnimatedImageDecodeManager
|
||||
|
||||
- (instancetype)initWith:(QGBaseDFileInfo *)fileInfo
|
||||
config:(QGAnimatedImageDecodeConfig *)config
|
||||
delegate:(id<QGAnimatedImageDecoderDelegate>)delegate {
|
||||
|
||||
if (self = [super init]) {
|
||||
|
||||
_config = config;
|
||||
_fileInfo = fileInfo;
|
||||
_decoderDelegate = delegate;
|
||||
[self createDecodersByConfig:config];
|
||||
_bufferManager = [[QGAnimatedImageBufferManager alloc] initWithConfig:config];
|
||||
[self initializeBuffers];
|
||||
[self setupAudioPlayerIfNeed];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
取出已解码的一帧并准备下一帧
|
||||
|
||||
@param frameIndex 帧索引
|
||||
@return 帧内容
|
||||
*/
|
||||
- (QGBaseAnimatedImageFrame *)consumeDecodedFrame:(NSInteger)frameIndex {
|
||||
|
||||
@synchronized (self) {
|
||||
// 控制何时命中第一帧,缓存满了才命中
|
||||
if (frameIndex == 0 && _bufferManager.buffers.count < _config.bufferCount) {
|
||||
return nil;
|
||||
}
|
||||
[self checkIfDecodeFinish:frameIndex];
|
||||
QGBaseAnimatedImageFrame *frame = [_bufferManager popVideoFrame];
|
||||
if (frame) {
|
||||
// pts顺序
|
||||
frame.frameIndex = frameIndex;
|
||||
[self decodeFrame:frameIndex+_config.bufferCount];
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tryToStartAudioPlay {
|
||||
if (!_audioPlayer) {
|
||||
return ;
|
||||
}
|
||||
[_audioPlayer play];
|
||||
}
|
||||
|
||||
#pragma mark - private methods
|
||||
|
||||
- (void)checkIfDecodeFinish:(NSInteger)frameIndex {
|
||||
|
||||
NSInteger decoderIndex = _decoders.count==1?0:frameIndex%_decoders.count;
|
||||
QGBaseDecoder *decoder = _decoders[decoderIndex];
|
||||
if ([decoder isFrameIndexBeyondEnd:frameIndex]) {
|
||||
if ([self.decoderDelegate respondsToSelector:@selector(decoderDidFinishDecode:)]) {
|
||||
[self.decoderDelegate decoderDidFinishDecode:decoder];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)decodeFrame:(NSInteger)frameIndex {
|
||||
|
||||
if (!_decoders || _decoders.count == 0) {
|
||||
//NSLog(@"error! can't find decoder");
|
||||
return ;
|
||||
}
|
||||
NSInteger decoderIndex = _decoders.count==1?0:frameIndex%_decoders.count;
|
||||
QGBaseDecoder *decoder = _decoders[decoderIndex];
|
||||
if ([decoder shouldStopDecode:frameIndex]) {
|
||||
return ;
|
||||
}
|
||||
[decoder decodeFrame:frameIndex buffers:_bufferManager.buffers];
|
||||
}
|
||||
|
||||
- (void)createDecodersByConfig:(QGAnimatedImageDecodeConfig *)config {
|
||||
|
||||
if (!self.decoderDelegate || ![self.decoderDelegate respondsToSelector:@selector(decoderClassForManager:)]) {
|
||||
VAP_Event(kQGVAPModuleCommon, @"you MUST implement the delegate in invoker!");
|
||||
NSAssert(0, @"you MUST implement the delegate in invoker!");
|
||||
return ;
|
||||
}
|
||||
|
||||
_decoders = [QGVAPSafeMutableArray new];
|
||||
for (int i = 0; i < config.threadCount; i ++) {
|
||||
Class class = [self.decoderDelegate decoderClassForManager:self];
|
||||
NSError *error = nil;
|
||||
QGBaseDecoder *decoder = [class alloc];
|
||||
decoder = [decoder initWith:_fileInfo error:&error];
|
||||
if (!decoder) {
|
||||
if ([self.decoderDelegate respondsToSelector:@selector(decoderDidFailDecode:error:)]) {
|
||||
[self.decoderDelegate decoderDidFailDecode:nil error:error];
|
||||
}
|
||||
break ;
|
||||
}
|
||||
[_decoders addObject:decoder];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)initializeBuffers {
|
||||
|
||||
for (int i = 0; i < _config.bufferCount; i++) {
|
||||
[self decodeFrame:i];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupAudioPlayerIfNeed {
|
||||
|
||||
if ([_fileInfo isKindOfClass:[QGMP4HWDFileInfo class]]) {
|
||||
QGMP4ParserProxy *mp4Parser = [(QGMP4HWDFileInfo *)_fileInfo mp4Parser];
|
||||
if (!mp4Parser.audioTrackBox) {
|
||||
_audioPlayer = nil;
|
||||
return ;
|
||||
}
|
||||
NSError *error;
|
||||
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL URLWithString:_fileInfo.filePath] error:&error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
// QGAnimatedImageDecodeThread.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface QGAnimatedImageDecodeThread : NSThread
|
||||
|
||||
@property (nonatomic, assign) BOOL occupied; //是否被解码器占用
|
||||
@property (nonatomic, readonly) NSString *sequenceDec; //线程标识信息
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
// QGAnimatedImageDecodeThread.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGAnimatedImageDecodeThread.h"
|
||||
|
||||
@implementation QGAnimatedImageDecodeThread
|
||||
|
||||
- (NSString *)sequenceDec
|
||||
{
|
||||
#ifdef DEBUG
|
||||
return [NSString stringWithFormat:@"%@",@([[self valueForKeyPath:@"private.seqNum"] integerValue])];//
|
||||
#else
|
||||
return [self description];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
// QGAnimatedImageDecodeThreadPool.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "QGAnimatedImageDecodeThread.h"
|
||||
|
||||
@interface QGAnimatedImageDecodeThreadPool : NSObject
|
||||
|
||||
+ (instancetype)sharedPool;
|
||||
- (QGAnimatedImageDecodeThread *)getDecodeThread;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,77 @@
|
||||
// QGAnimatedImageDecodeThreadPool.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGAnimatedImageDecodeThreadPool.h"
|
||||
#import "QGAnimatedImageDecodeThread.h"
|
||||
#import "QGVAPSafeMutableArray.h"
|
||||
|
||||
@interface QGAnimatedImageDecodeThreadPool (){
|
||||
|
||||
NSMutableArray *_threads;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation QGAnimatedImageDecodeThreadPool
|
||||
|
||||
+ (instancetype)sharedPool {
|
||||
|
||||
static QGAnimatedImageDecodeThreadPool *instance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[QGAnimatedImageDecodeThreadPool alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
|
||||
if (self = [super init]) {
|
||||
_threads = [QGVAPSafeMutableArray new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
从池子中找出没被占用的线程,如果没有则新建一个
|
||||
|
||||
@return 解码线程
|
||||
*/
|
||||
- (QGAnimatedImageDecodeThread *)getDecodeThread {
|
||||
|
||||
QGAnimatedImageDecodeThread *freeThread = nil;
|
||||
for (QGAnimatedImageDecodeThread *thread in _threads) {
|
||||
if (!thread.occupied) {
|
||||
freeThread = thread;
|
||||
}
|
||||
}
|
||||
if (!freeThread) {
|
||||
freeThread = [[QGAnimatedImageDecodeThread alloc] initWithTarget:self selector:@selector(run) object:nil];
|
||||
[freeThread start];
|
||||
[_threads addObject:freeThread];
|
||||
}
|
||||
return freeThread;
|
||||
}
|
||||
|
||||
- (void)run{
|
||||
//线程保活
|
||||
@autoreleasepool {
|
||||
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
|
||||
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
|
||||
[runLoop run];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,44 @@
|
||||
// QGVAPConfigManager.h
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "QGMP4HWDFileInfo.h"
|
||||
#import "QGVAPConfigModel.h"
|
||||
#import "VAPMacros.h"
|
||||
|
||||
@class QGVAPSourceInfo;
|
||||
@protocol QGVAPConfigDelegate <NSObject>
|
||||
|
||||
- (void)onVAPConfigResourcesLoaded:(QGVAPConfigModel *)config error:(NSError *)error;
|
||||
|
||||
@optional
|
||||
- (NSString *)vap_contentForTag:(NSString *)tag resource:(QGVAPSourceInfo *)info; //替换配置中的资源占位符(不处理直接返回tag)
|
||||
- (void)vap_loadImageWithURL:(NSString *)urlStr context:(NSDictionary *)context completion:(VAPImageCompletionBlock)completionBlock;
|
||||
|
||||
@end
|
||||
|
||||
@interface QGVAPConfigManager : NSObject
|
||||
|
||||
@property (nonatomic, weak) id<QGVAPConfigDelegate> delegate;
|
||||
@property (nonatomic, assign) BOOL hasValidConfig;
|
||||
@property (nonatomic, strong) QGVAPConfigModel *model;
|
||||
|
||||
- (instancetype)initWith:(QGMP4HWDFileInfo *)fileInfo;
|
||||
- (void)loadConfigResources;
|
||||
- (void)loadMTLTextures:(id<MTLDevice>)device;
|
||||
- (void)loadMTLBuffers:(id<MTLDevice>)device;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// QGVAPConfigManager.m
|
||||
// Tencent is pleased to support the open source community by making vap available.
|
||||
//
|
||||
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License"); you may not use this file except in
|
||||
// compliance with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
// either express or implied. See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "QGVAPConfigManager.h"
|
||||
#import "QGMP4Parser.h"
|
||||
#import "QGVAPLogger.h"
|
||||
#import "NSDictionary+VAPUtil.h"
|
||||
#import "UIColor+VAPUtil.h"
|
||||
#import "NSArray+VAPUtil.h"
|
||||
#import "QGHWDMetalRenderer.h"
|
||||
#import "QGVAPTextureLoader.h"
|
||||
|
||||
@interface QGVAPConfigManager () {
|
||||
|
||||
QGMP4HWDFileInfo *_fileInfo;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation QGVAPConfigManager
|
||||
|
||||
- (instancetype)initWith:(QGMP4HWDFileInfo *)fileInfo {
|
||||
|
||||
if (self = [super init]) {
|
||||
_fileInfo = fileInfo;
|
||||
[self setupConfig];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupConfig {
|
||||
|
||||
QGMP4Box *vapc = [_fileInfo.mp4Parser.rootBox subBoxOfType:QGMP4BoxType_vapc];
|
||||
if (!vapc) {
|
||||
self.hasValidConfig = NO;
|
||||
return ;
|
||||
}
|
||||
self.hasValidConfig = YES;
|
||||
NSData *vapcData = [_fileInfo.mp4Parser readDataOfBox:vapc length:vapc.length-8 offset:8];
|
||||
NSError *error = nil;
|
||||
NSDictionary *configDictionary = [NSJSONSerialization JSONObjectWithData:vapcData options:kNilOptions error:&error];
|
||||
if (error) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"fail to parse config as dictionary file %@", vapc);
|
||||
}
|
||||
[self parseConfigDictinary:configDictionary];
|
||||
}
|
||||
|
||||
#pragma mark - resource loader
|
||||
|
||||
- (void)loadConfigResources {
|
||||
|
||||
if (self.model.resources.count == 0) {
|
||||
if ([self.delegate respondsToSelector:@selector(onVAPConfigResourcesLoaded:error:)]) {
|
||||
[self.delegate onVAPConfigResourcesLoaded:self.model error:nil];
|
||||
}
|
||||
return ;
|
||||
}
|
||||
|
||||
//tags
|
||||
if ([self.delegate respondsToSelector:@selector(vap_contentForTag:resource:)]) {
|
||||
[self.model.resources enumerateObjectsUsingBlock:^(QGVAPSourceInfo * _Nonnull resource, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
resource.contentTagValue = [self.delegate vap_contentForTag:resource.contentTag resource:resource];
|
||||
}];
|
||||
}
|
||||
|
||||
if (![self.delegate respondsToSelector:@selector(vap_loadImageWithURL:context:completion:)]) {
|
||||
return ;
|
||||
}
|
||||
__block NSError *loadError = nil;
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
[self.model.resources enumerateObjectsUsingBlock:^(QGVAPSourceInfo * _Nonnull resource, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
|
||||
NSString *tagContent = resource.contentTagValue;
|
||||
if ([resource.type isEqualToString:kQGAGAttachmentSourceTypeText] && [resource.loadType isEqualToString:QGAGAttachmentSourceLoadTypeLocal]) {
|
||||
resource.sourceImage = [QGVAPTextureLoader drawingImageForText:tagContent color:resource.color size:resource.size bold:[resource.style isEqualToString:kQGAGAttachmentSourceStyleBoldText]];
|
||||
}
|
||||
|
||||
if ([resource.type isEqualToString:kQGAGAttachmentSourceTypeImg] && [resource.loadType isEqualToString:QGAGAttachmentSourceLoadTypeNet]) {
|
||||
NSString *imageURL = tagContent;
|
||||
|
||||
NSDictionary *context = @{@"resource":resource};
|
||||
dispatch_group_enter(group);
|
||||
[self.delegate vap_loadImageWithURL:imageURL context:context completion:^(UIImage *image, NSError *error, NSString *imageURL) {
|
||||
if (!image || error) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"loadImageWithURL %@ error:%@", imageURL, error);
|
||||
loadError = (loadError ?: (error ?: ([NSError errorWithDomain:[NSString stringWithFormat:@"loadImageError:%@", imageURL] code:-1 userInfo:nil])));
|
||||
}
|
||||
resource.sourceImage = image;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
}];
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
if ([self.delegate respondsToSelector:@selector(onVAPConfigResourcesLoaded:error:)]) {
|
||||
[self.delegate onVAPConfigResourcesLoaded:self.model error:loadError];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)loadMTLTextures:(id<MTLDevice>)device {
|
||||
|
||||
[self.model.resources enumerateObjectsUsingBlock:^(QGVAPSourceInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
id<MTLTexture> texture = [QGVAPTextureLoader loadTextureWithImage:obj.sourceImage device:device];
|
||||
obj.sourceImage = nil;
|
||||
obj.texture = texture;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)loadMTLBuffers:(id<MTLDevice>)device {
|
||||
|
||||
[self.model.resources enumerateObjectsUsingBlock:^(QGVAPSourceInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
id<MTLBuffer> buffer = [QGVAPTextureLoader loadVapColorFillBufferWith:obj.color device:device];
|
||||
obj.colorParamsBuffer = buffer;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - parse json
|
||||
|
||||
- (void)parseConfigDictinary:(NSDictionary *)configDic {
|
||||
|
||||
NSDictionary *commonInfoDic = [configDic hwd_dicValue:@"info"];
|
||||
NSArray *sourcesArr = [configDic hwd_arrValue:@"src"];
|
||||
NSArray *framesArr = [configDic hwd_arrValue:@"frame"];
|
||||
|
||||
if (!commonInfoDic) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"has no commonInfoDic:%@", configDic);
|
||||
return ;
|
||||
}
|
||||
QGVAPConfigModel *configModel = [QGVAPConfigModel new];
|
||||
//parse
|
||||
NSInteger version = [commonInfoDic hwd_integerValue:@"v"];
|
||||
NSInteger frameCount = [commonInfoDic hwd_integerValue:@"f"];
|
||||
CGFloat w = [commonInfoDic hwd_floatValue:@"w"];
|
||||
CGFloat h = [commonInfoDic hwd_floatValue:@"h"];
|
||||
CGFloat video_w = [commonInfoDic hwd_floatValue:@"videoW"];
|
||||
CGFloat video_h = [commonInfoDic hwd_floatValue:@"videoH"];
|
||||
CGFloat orientaion = [commonInfoDic hwd_integerValue:@"orien"];
|
||||
NSInteger fps = [commonInfoDic hwd_integerValue:@"fps"];
|
||||
BOOL isMerged = ([commonInfoDic hwd_integerValue:@"isVapx"] == 1);
|
||||
NSArray *a_frame = [commonInfoDic hwd_arrValue:@"aFrame"];
|
||||
NSArray *rgb_frame = [commonInfoDic hwd_arrValue:@"rgbFrame"];
|
||||
self.model = configModel;
|
||||
//整体信息
|
||||
QGVAPCommonInfo *commonInfo = [QGVAPCommonInfo new];
|
||||
commonInfo.version = version;
|
||||
commonInfo.framesCount = frameCount;
|
||||
commonInfo.size = CGSizeMake(w, h);
|
||||
commonInfo.videoSize = CGSizeMake(video_w, video_h);
|
||||
commonInfo.targetOrientaion = orientaion;
|
||||
commonInfo.fps = fps;
|
||||
commonInfo.isMerged = isMerged;
|
||||
commonInfo.alphaAreaRect = a_frame ? [a_frame hwd_rectValue] : CGRectZero;
|
||||
commonInfo.rgbAreaRect = rgb_frame ? [rgb_frame hwd_rectValue] : CGRectZero;
|
||||
configModel.info = commonInfo;
|
||||
|
||||
if (!sourcesArr) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"has no sourcesArr:%@", configDic);
|
||||
return ;
|
||||
}
|
||||
|
||||
//源信息
|
||||
NSMutableDictionary <NSString *, QGVAPSourceInfo *>*sources = [NSMutableDictionary new];
|
||||
[sourcesArr enumerateObjectsUsingBlock:^(NSDictionary *sourceDic, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if (![sourceDic isKindOfClass:[NSDictionary class]]) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"sourceDic is not dic:%@", sourceDic);
|
||||
return ;
|
||||
}
|
||||
NSString *sourceID = [sourceDic hwd_stringValue:@"srcId"];
|
||||
if (!sourceID) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"has no sourceID:%@", sourceDic);
|
||||
return ;
|
||||
}
|
||||
//parse
|
||||
QGAGAttachmentSourceType sourceType = [sourceDic hwd_stringValue:@"srcType"];
|
||||
QGAGAttachmentSourceLoadType loadType = [sourceDic hwd_stringValue:@"loadType"];
|
||||
NSString *contentTag = [sourceDic hwd_stringValue:@"srcTag"];
|
||||
UIColor *color = [UIColor hwd_colorWithHexString:[sourceDic hwd_stringValue:@"color"]];
|
||||
QGAGAttachmentSourceStyle style = [sourceDic hwd_stringValue:@"style"];
|
||||
CGFloat width = [sourceDic hwd_floatValue:@"w"];
|
||||
CGFloat height = [sourceDic hwd_floatValue:@"h"];
|
||||
QGAGAttachmentFitType fitType = [sourceDic hwd_stringValue:@"fitType"];
|
||||
QGVAPSourceInfo *sourceInfo = [QGVAPSourceInfo new];
|
||||
sourceInfo.type = sourceType;
|
||||
sourceInfo.style = style;
|
||||
sourceInfo.contentTag = contentTag;
|
||||
sourceInfo.color = color;
|
||||
sourceInfo.size = CGSizeMake(width, height);
|
||||
sourceInfo.fitType = fitType;
|
||||
sourceInfo.loadType = loadType;
|
||||
sources[sourceID] = sourceInfo;
|
||||
}];
|
||||
configModel.resources = sources.allValues;
|
||||
|
||||
//融合信息
|
||||
if (!framesArr) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"has no framesArr:%@", configDic);
|
||||
return ;
|
||||
}
|
||||
NSMutableDictionary <NSNumber *, NSArray<QGVAPMergedInfo *>*> *mergedConfig = [NSMutableDictionary new];
|
||||
[framesArr enumerateObjectsUsingBlock:^(NSDictionary *frameMergedDic, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if (![frameMergedDic isKindOfClass:[NSDictionary class]]) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"frameMergedDic is not dic:%@", frameMergedDic);
|
||||
return ;
|
||||
}
|
||||
NSInteger frameIndex = [frameMergedDic hwd_integerValue:@"i"];
|
||||
|
||||
NSMutableArray <QGVAPMergedInfo *> *mergedInfos = [NSMutableArray new];
|
||||
NSArray *mergedObjs = [frameMergedDic hwd_arrValue:@"obj"];
|
||||
[mergedObjs enumerateObjectsUsingBlock:^(NSDictionary *mergeInfoDic, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if (![mergeInfoDic isKindOfClass:[NSDictionary class]]) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"mergeInfoDic is not dic:%@", mergeInfoDic);
|
||||
return ;
|
||||
}
|
||||
NSString *sourceID = [mergeInfoDic hwd_stringValue:@"srcId"];
|
||||
QGVAPSourceInfo *sourceInfo = sources[sourceID];
|
||||
if (!sourceInfo) {
|
||||
VAP_Error(kQGVAPModuleCommon, @"sourceInfo is nil:%@", mergeInfoDic);
|
||||
return ;
|
||||
}
|
||||
//parse
|
||||
NSArray *frame = [mergeInfoDic hwd_arrValue:@"frame"];
|
||||
NSArray *m_frame = [mergeInfoDic hwd_arrValue:@"mFrame"];
|
||||
NSInteger renderIndex = [mergeInfoDic hwd_integerValue:@"z"];
|
||||
NSInteger rotationAngle = [mergeInfoDic hwd_integerValue:@"mt"];
|
||||
QGVAPMergedInfo *mergeInfo = [QGVAPMergedInfo new];
|
||||
mergeInfo.source = sourceInfo;
|
||||
mergeInfo.renderIndex = renderIndex;
|
||||
mergeInfo.needMask = (m_frame != nil);
|
||||
mergeInfo.renderRect = frame ? [frame hwd_rectValue] : CGRectZero;
|
||||
mergeInfo.maskRect = m_frame ? [m_frame hwd_rectValue] : CGRectZero;
|
||||
mergeInfo.maskRotation = rotationAngle;
|
||||
[mergedInfos addObject:mergeInfo];
|
||||
}];
|
||||
NSArray *sortedMergeInfos = [mergedInfos sortedArrayUsingComparator:^NSComparisonResult(QGVAPMergedInfo *info1, QGVAPMergedInfo *info2) {
|
||||
return [@(info1.renderIndex) compare:@(info2.renderIndex)];
|
||||
}];
|
||||
mergedConfig[@(frameIndex)] = sortedMergeInfos;
|
||||
}];
|
||||
configModel.mergedConfig = mergedConfig;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user