AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)

版本记录

版本号 时间
V1.0 2017.09.01

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 视频音频的合成(一)
7. AVFoundation框架解析(七)—— 视频组合和音频混合调试
8. AVFoundation框架解析(八)—— 优化用户的播放体验
9. AVFoundation框架解析(九)—— AVFoundation的变化(一)
10. AVFoundation框架解析(十)—— AVFoundation的变化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的变化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的变化(四)
13. AVFoundation框架解析(十三)—— 构建基本播放应用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)

AVAssetReader Reading Timecode

AVAssetReader对象用于获取资产的媒体数据。 读取存储在时间码轨道中的时间码媒体样本以与用于使用AVAssetReader读取任何其他媒体(如音频或视频媒体)相同的方式执行。

将AVAssetReader对象与要读取的资产分配后,为时间码轨道创建一个AVAssetReaderTrackOutput,然后调用 - (BOOL)startReading来准备读取器从资产读取样本缓冲区。 然后将- (CMSampleBufferRef)copyNextSampleBuffer方法发送到轨道输出对象以接收时间码采样。

包含时间码示例的返回的CMSampleBufferRef可以被解释为应用程序所需的,例如,返回的帧号可以转换为CVSMPTETime表示。 有关实用程序功能,请参阅本文档的“时间码实用程序函数”部分,该功能允许您执行kCMTimeCodeFormatType_TimeCode32时间码样本格式类型的转换。 检索描述时间码示例调用CMSampleBufferGetFormatDescription的格式详细信息的格式说明。

下面代码展示了如何为时间码媒体轨道创建AVAssetReader对象和AVAssetReaderTrackOutput对象。

...
 
// Create asset reader
assetReader = [[AVAssetReader alloc] initWithAsset:localAsset error:&localError];
success = (assetReader != nil);
 
// Create asset reader output for the first timecode track of the asset
if (success) {
    AVAssetTrack *timecodeTrack = nil;
 
    // Grab first timecode track, if the asset has them
    NSArray *timecodeTracks = [localAsset tracksWithMediaType:AVMediaTypeTimecode];
    if ([timecodeTracks count] > 0)
        timecodeTrack = [timecodeTracks objectAtIndex:0];
 
    if (timecodeTrack) {
        timecodeOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:timecodeTrack outputSettings:nil];
        [assetReader addOutput:timecodeOutput];
    } else {
        NSLog(@"%@ has no timecode tracks", localAsset);
    }
}
 
...

下面提供了两种方法。 第一个演示如何从AVAssetReaderTrackOutput读取采样缓冲区,第二个演示如何检索采样数据并进行解释。 在这种情况下,从本文档的Timecode Utility功能部分调用其中一个时间码实用程序函数,将采样数据转换为CVSMPTETime,然后简单地输出值。

//  Read a Timecode Sample Buffer and print out the CVSMPTETime.

- (BOOL)startReadingAndPrintingOutputReturningError:(NSError **)outError
{
    BOOL success = YES;
    NSError *localError = nil;
 
    // Instruct the asset reader to get ready to do work
    success = [assetReader startReading];
 
    if (!success) {
        localError = [assetReader error];
    } else {
        CMSampleBufferRef currentSampleBuffer = NULL;
 
        while ((currentSampleBuffer = [timecodeOutput copyNextSampleBuffer])) {
            [self outputTimecodeDescriptionForSampleBuffer:currentSampleBuffer];
        }
 
        if (currentSampleBuffer) {
            CFRelease(currentSampleBuffer);
        }
    }
 
    if (!success && outError)
        *outError = localError;
 
    return success;
}
 
- (void)outputTimecodeDescriptionForSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    CMFormatDescriptionRef formatDescription =  CMSampleBufferGetFormatDescription(sampleBuffer);
 
    if (blockBuffer && formatDescription) {
 
        size_t length = 0;
        size_t totalLength = 0;
        char *rawData = NULL;
 
        OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, &length, &totalLength, &rawData);
        if (status != kCMBlockBufferNoErr) {
            NSLog(@"Could not get data from block buffer");
        }
        else {
 
            CMMediaType type = CMFormatDescriptionGetMediaSubType(formatDescription);
 
            if (type == kCMTimeCodeFormatType_TimeCode32) {
                int32_t *frameNumberRead = (int32_t *)rawData;
                CVSMPTETime timecode = timecodeForFrameNumber32UsingFormatDescription(*frameNumberRead, formatDescription);
 
                BOOL dropFrame = CMTimeCodeFormatDescriptionGetTimeCodeFlags(formatDescription) & kCMTimeCodeFlag_DropFrame;
                char separator = dropFrame ? ',' : '.';
                NSLog(@"%@",[NSString stringWithFormat:@"HH:MM:SS%cFF => %02d:%02d:%02d%c%02d (frame number: %d)", separator, timecode.hours, timecode.minutes, timecode.seconds,  separator, timecode.frames, (int)Endian32_Swap(*frameNumberRead)]);
            }
        }
    }
}

时间码实用函数

下面代码提供了实用程序函数,演示了如何将CVSMPTETime转换为帧编号,并将帧编号转换为kCMTimeCodeFormatType_TimeCode32时间代码媒体样本格式的CVSMPTETime

enum {
    tcNegativeFlag = 0x80    /* negative bit is in minutes */
};
//CVSMPTETime to Frame Number (kCMTimeCodeFormatType_TimeCode32 Media Sample)

int32_t frameNumber32ForTimecodeUsingFormatDescription(CVSMPTETime timecode, CMTimeCodeFormatDescriptionRef formatDescription)
{
    int32_t frameNumber = 0;
 
    if (CMTimeCodeFormatDescriptionGetFormatType(formatDescription) == kCMTimeCodeFormatType_TimeCode32) {
        int32_t frameQuanta = CMTimeCodeFormatDescriptionGetFrameQuanta(formatDescription);
 
        frameNumber = timecode.frames;
        frameNumber += timecode.seconds * frameQuanta;
        frameNumber += (timecode.minutes & ~tcNegativeFlag) * frameQuanta * 60;
        frameNumber += timecode.hours * frameQuanta * 60 * 60;
 
        int32_t fpm = frameQuanta * 60;
 
        if (CMTimeCodeFormatDescriptionGetTimeCodeFlags(formatDescription) & kCMTimeCodeFlag_DropFrame) {
            int32_t fpm10 = fpm * 10;
            int32_t num10s = frameNumber / fpm10;
            int32_t frameAdjust = -num10s*(9*2);
            int32_t numFramesLeft = frameNumber % fpm10;
 
            if (numFramesLeft > 1) {
                int32_t num1s = numFramesLeft / fpm;
                if (num1s > 0) {
                    frameAdjust -= (num1s-1)*2;
                    numFramesLeft = numFramesLeft % fpm;
                    if (numFramesLeft > 1)
                        frameAdjust -= 2;
                    else
                        frameAdjust -= (numFramesLeft+1);
                }
            }
            frameNumber += frameAdjust;
        }
 
        if (timecode.minutes & tcNegativeFlag) {
            frameNumber = -frameNumber;
        }
    }
 
    return EndianS32_NtoB(frameNumber);
}
// Frame Number (kCMTimeCodeFormatType_TimeCode32 Media Sample) to CVSMPTETime

CVSMPTETime timecodeForFrameNumber32UsingFormatDescription(int32_t frameNumber, CMTimeCodeFormatDescriptionRef formatDescription)
{
    CVSMPTETime timecode = {0};
 
    if (CMTimeCodeFormatDescriptionGetFormatType(formatDescription) == kCMTimeCodeFormatType_TimeCode32) {
        frameNumber = EndianS32_BtoN(frameNumber);
 
        short fps = CMTimeCodeFormatDescriptionGetFrameQuanta(formatDescription);
        BOOL neg = FALSE;
 
        if (frameNumber < 0) {
            neg = TRUE;
            frameNumber = -frameNumber;
        }
 
        if (CMTimeCodeFormatDescriptionGetTimeCodeFlags(formatDescription) & kCMTimeCodeFlag_DropFrame) {
            int32_t fpm = fps*60 - 2;
            int32_t fpm10 = fps*10*60 - 9*2;
            int32_t num10s = frameNumber / fpm10;
            int32_t frameAdjust = num10s*(9*2);
            int32_t numFramesLeft = frameNumber % fpm10;
 
            if (numFramesLeft >= fps*60) {
                numFramesLeft -= fps*60;
                int32_t num1s = numFramesLeft / fpm;
                frameAdjust += (num1s+1)*2;
            }
            frameNumber += frameAdjust;
        }
 
        timecode.frames = frameNumber % fps;
        frameNumber /= fps;
        timecode.seconds = frameNumber % 60;
        frameNumber /= 60;
        timecode.minutes = frameNumber % 60;
        frameNumber /= 60;
 
        if (CMTimeCodeFormatDescriptionGetTimeCodeFlags(formatDescription) & kCMTimeCodeFlag_24HourMax) {
            frameNumber %= 24;
            if (neg && !(CMTimeCodeFormatDescriptionGetTimeCodeFlags(formatDescription) & kCMTimeCodeFlag_NegTimesOK)) {
                neg = FALSE;
                frameNumber = 23 - frameNumber;
            }
        }
        timecode.hours = frameNumber;
        if (neg) {
            timecode.minutes |= tcNegativeFlag;
        }
 
        timecode.flags = kCVSMPTETimeValid;
    }
 
    return timecode;
}

关于TimeCode64格式类型

在构建基于AVFoundation的媒体应用程序时,建议使用kCMTimeCodeFormatType_TimeCode64('tc64')格式,而使用本文档中讨论的kCMTimeCodeFormatType_TimeCode32格式应被视为具有与旧版基于QuickTime的媒体应用程序具有特定互操作性要求的应用程序的解决方案, 支持'tmcd'时间码采样格式。

kCMTimeCodeFormatType_TimeCode64格式的媒体采样存储为Big-Endian SInt64

注意:'tc64'时间采样码数据格式。

CMTimeCodeFormatType_TimeCode64 ('tc64') Timecode Sample Data Format.
 
The timecode media sample data format is a big-endian signed 64-bit integer representing a frame number that is typically converted to and from SMPTE timecodes representing hours, minutes, seconds, and frames, according to information carried in the format description.
 
Converting to and from the frame number stored as media sample data and a CVSMPTETime structure is performed using simple modular arithmetic with the expected adjustments for drop frame timecode performed using information in the format description such as the frame quanta and the drop frame flag.
 
The frame number value may be interpreted into a timecode value as follows:
 
Hours
A 16-bit signed integer that indicates the starting number of hours.
 
Minutes
A 16-bit signed integer that contains the starting number of minutes.
 
Seconds
A 16-bit signed integer indicating the starting number of seconds.
 
Frames
A 16-bit signed integer that specifies the starting number of frames. This field’s value cannot exceed the value of the frame quanta value in the timecode format description.

后记

未完,待续~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容