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框架解析(十三)—— 构建基本播放应用程序

VAssetWriter和AVAssetReader的Timecode支持

AVFoundation包括对时间码轨道的支持。 时间码轨道允许您在QuickTime影片文件(.mov)中存储外部时间码信息,如SMPTE时间码。 本技术说明讨论如何使用AVAssetWriterAVAssetReader编写和读取32位时间码('tmcd')媒体样本。


先决条件

本技术说明假设读者熟悉AVFoundation,特别是AVAssetWriterAVAssetReader类。 还假定了CoreMedia Sample Buffer API的一些背景。


关于Timecodes

QuickTime电影文件(.mov)允许存储从影片的原始素材资源导出的时间信息,例如影响媒体如何解释和播放的帧持续时间。

QuickTime影片文件还允许您存储不会特别影响媒体播放的附加时间信息。 这个额外的时间信息一般来自原始的来源资料; 例如,作为SMPTE时间码。 实质上,您可以将时间码视为提供媒体特定时间信息与原始来源资料的时间信息之间的链接。

QuickTime影片文件的时间码信息存储在时间码轨道中。 时间码轨道包含由以下内容组成的格式描述。

  • 时间码格式信息,指定时间码的特征以及如何解释时间码信息。
  • 帧号作为从AVFoundation时间值的给定电影帧映射到其对应的时间码值的方式。
  • 源标识信息,其可选地可以标识源名称,例如源卷轴,片段或项目的名称。

存储在轨道中的信息的方式是独立于任何特定的时间码标准。 该信息的格式足够灵活以适应所有已知的时间码标准,包括作为本技术说明的重点的SMPTE时间码。 时间码格式信息为AVFoundation用户提供了理解时间码和将时间值转换为时间码时间值的参数,反之亦然。

一个关键的时间码属性涉及用于将时间码值与视频帧同步的技术。 大多数视频源素材以全码帧速率记录。 例如,PALSECAM视频都包含每秒25帧。 然而,一些视频源素材不以全码帧速率记录。 特别地,NTSC彩色视频包含每秒29.97帧(尽管通常被称为30帧/秒视频)。 然而,NTSC时间码值对应于全30帧/秒速率; 这是NTSC黑白视频的延续。 对于这样的视频源,您需要一种机制来纠正在时间码值和实际视频帧之间随时间推移的错误。

维持时间码值和视频数据之间同步的常用方法称为丢帧。 与其名称相反,丢帧技术实际上以预定速率跳过时间码值,以便保持时间码和视频数据的同步。 它实际上不会丢弃视频帧。 在使用dropframe技术的NTSC彩色视频中,时间码值每分钟跳过两个帧值,除了可以被10整除的微小值之外。 因此,以HH:MM:SS:FF(小时,分钟,秒,帧)表示的NTSC时间码值,从00:00:59:29跳至00:01:00:02(跳过00:01:00:0000:01:00:01)。 在时间码格式描述中有一个标志,指示时间码是否使用丢帧技术。


AVAssetWriter Writing Timecode

AVAssetWriter对象用于将媒体数据写入指定的视听容器类型的新文件。 对于时间码,这应该是一个QuickTime电影文件(.mov),它在AVFoundation中定义为AVFileTypeQuickTimeMovie

以与用于使用AVAssetWriter添加音频或视频媒体的任何其他轨道类型相同的方式来执行创建时间码轨道和附加时间码媒体。 为AVFileTypeQuickTimeMovie文件类型创建AVAssetWriter。 使用时间码媒体类型AVMediaTypeTimecode创建AVAssetWriterInput,以写入时间码轨道。 通过调用CMTimeCodeFormatDescriptionCreate来形成描述正在使用的特定时间码介质类型(kCMTimeCodeFormatType_TimeCode32是最常见的)的格式描述。 此格式描述定义您将添加到轨道的时间码数据格式。 然后使用核心媒体采样缓冲区API(如CMSampleBufferCreate)创建采样媒体缓冲区,最后使用AVAssetWriterInput - (BOOL)appendSampleBuffer((CMSampleBufferRef)sampleBuffer)方法将时间码媒体采样缓冲区添加到时间码轨道。

1. Creating a Timecode Track - 创建Timecode轨道

要创建时间码轨道,请使用AVMediaTypeTimecode媒体类型创建AVAssetWriterInput

当使用时间码轨道时,您还必须定义时间码轨道与时间码所关联的一个或多个视频轨道之间的关系。 这是通过使用视频AVAssetWriterInput对象完成的(void)addTrackAssociationWithTrackOfInput :( AVAssetWriterInput *)输入类型:(NSString *)trackAssociationType方法设置此关联轨道。

下面代码演示了如何为视频和时间码轨道创建AVAssetWriter和AVAssetWriterInput对象,以及如何设置轨道关联。

AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:localOutputURL
                  fileType:AVFileTypeQuickTimeMovie error:&localError];
 
...
 
// Setup video track to write video samples into
AVAssetWriterInput *videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:
                                    [videoTrack mediaType] outputSettings:nil];
 
[assetWriter addInput:videoInput];
 
...
 
// Setup timecode track to write timecode samples into
AVAssetWriterInput *timecodeInput =
                        [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeTimecode
                            outputSettings:nil];
 
// add track association with video track
[videoInput addTrackAssociationWithTrackOfInput:timecodeInput
    type:AVTrackAssociationTypeTimecode];
 
[assetWriter addInput:timecodeInput];

时间码轨道中的每个样本都提供电影时间跨度的时间码信息。 时间码媒体样本包括持续时间信息。 因此,您通常在创建相应的内容轨道或轨道后添加每个时间码示例。

这里还要注意:默认情况下,时间码的轨道尺寸设置为0.0 x 0.0。 如果需要更改此默认值,则AVAssetWriterInput属性naturalSize可用于设置轨道尺寸。

@property (nonatomic) CGSize naturalSize

2. The Timecode Format Description - 时间码格式描述

时间码媒体格式描述包含允许AVFoundation解释样本的控制信息。 实际的样本数据包含标识使用该时间码的一个或多个视频内容帧的帧号。 当使用格式类型kCMTimeCodeFormatType_TimeCode32时,作为Big-Endian int32_t存储,此值标识使用此时间代码示例的帧组中的第一帧。 在源材料不包含编辑的电影的情况下,您只需要一个样本。 当源素材包含编辑时,通常每个编辑都需要一个样本。 那些样本将包含开始每个新的帧组的帧的帧号。

要创建格式描述,请使用CMTimeCodeFormatDescriptionCreate

/*!
    @function   CMTimeCodeFormatDescriptionCreate
    @abstract   Creates a format description for a timecode media.
    @discussion The caller owns the returned CMFormatDescription,
                and must release it when done with it. All input parameters
                are copied (the extensions are deep-copied).
                The caller can deallocate them or re-use them after making this call.
*/
CM_EXPORT OSStatus CMTimeCodeFormatDescriptionCreate(
    CFAllocatorRef allocator,                   /*! @param allocator
                                                    Allocator to be used for creating the
                                                    FormatDescription object */
    CMTimeCodeFormatType timeCodeFormatType,    /*! @param timeCodeFormatType
                                                    One of the CMTimeCodeFormatTypes */
    CMTime frameDuration,                       /*! @param frameDuration
                                                    Duration of each frame (eg. 100/2997) */
    uint32_t frameQuanta,                       /*! @param frameQuanta
                                                    Frames/sec for timecode (eg. 30) OR
                                                    frames/tick for counter mode */
    uint32_t tcFlags,                           /*! @param tcFlags
                                                    kCMTimeCodeFlag_DropFrame,
                                                    kCMTimeCodeFlag_24HourMax,
                                                    kCMTimeCodeFlag_NegTimesOK */
    CFDictionaryRef extensions,                 /*! @param extensions
                                                    Keys are always CFStrings. Values are
                                                    always property list objects (ie. CFData).
                                                    May be NULL. */
    CMTimeCodeFormatDescriptionRef *descOut) /*! @param descOut
                                                Receives the newly-created CMFormatDescription. */
__OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);

时间码格式描述定义时间码媒体样本的格式和内容,由以下信息组成:

  • 时间码格式类型(CMTimeCodeFormatType) - 时间码格式类型之一,例如kCMTimeCodeFormatType_TimeCode32,它将样本类型描述为32位整数。
  • 帧持续时间(CMTime) - 每帧的持续时间(例如,100/2997)。 这指定了每个帧持续时间由时间标度定义的时间长度。
  • Frame Quanta(uint32_t) - 表示每秒存储的帧数,例如30。
  • 时间码标志(uint32_t) - 提供某些时间码格式信息的标志,例如kCMTimeCodeFlag_DropFrame,表示时间码偶尔丢帧以保持同步。 某些时间代码以每秒不超过一帧的帧数运行。 例如,NTSC视频以每秒29.97帧的速度运行。 为了在时间码速率和每秒30帧的重播速率之间重新同步,时间码在可预测的时间丢弃帧(与闰年保持日历同步的方式大致相同)。 如果时间码使用drop-frame技术,则将此标志设置为1。 其他标志包括kCMTimeCodeFlag_24HourMax,以指示时间码值在24小时内换行。 如果时间码小时值在24小时包装(即返回0),并将kCMTimeCodeFlag_NegTimesOK指定为时间码支持负时间值,则将此标志设置为1。 如果时间码允许负值,则将此标志设置为1。
  • 扩展(CFDictionary) - 提供源名称信息(kCMTimeCodeFormatDescriptionExtension_SourceReferenceName)的可选字典。 此扩展名是包含以下两个键的CFDictionary; kCMTimeCodeFormatDescriptionKey_Value一个CFStringkCMTimeCodeFormatDescriptionKey_LangCode一个CFNumber。 描述键可能包含创建电影的录像带的名称。

3. Creating a Timecode Format Description - 创建时间码格式描述

了解如何格式化和解释时间码格式描述的最佳方式是考虑一个例子。 如果您正在以每秒29.97帧记录的NTSC视频源创建电影,您将创建如下格式描述。

...
 
CMTimeCodeFormatDescriptionRef formatDescription = NULL;
uint32_t tcFlags = kCMTimeCodeFlag_DropFrame | kCMTimeCodeFlag_24HourMax;
 
OSStatus status = CMTimeCodeFormatDescriptionCreate(kCFAllocatorDefault,
                                                    kCMTimeCodeFormatType_TimeCode32,
                                                    CMTimeMake(100, 2997),
                                                    30,
                                                    tcFlags,
                                                    NULL,
                                                    &formatDescription);
...

通过将时间尺度值除以帧持续时间(2997/100),获得29.97帧每秒的电影的自然帧速率。 标志字段指示时间码使用丢帧技术来重新同步电影的每秒29.97帧的自然帧速率,其播放速率为每秒30帧。

4. The Timecode Media Sample - 时间码媒体采样

写入轨道的媒体样本包含标识使用该时间码的一个或多个视频帧的帧号。 当使用时间码格式类型kCMTimeCodeFormatType_TimeCode32时,此帧号存储为Big-Endian int32_t

给定时间码格式描述,您可以将帧号转换为SMPTE时间值,并将SMPTE时间值转换为帧号。 一个简单的例子是SMPTE时间值00:00:12:15(HH:MM:SS:FF)30fps,非丢帧,您将获得一个帧号375((12 * 30)+15)

当使用SMPTE时间值时,Core Video CVSMPTETime结构用于存储这些时间值。 CVSMPTETime结构允许您将时间信息解释为时间值(HH:MM:SS:FF),并定义如下

struct CVSMPTETime
{
    SInt16  subframes;
    SInt16  subframeDivisor;
    UInt32  counter;
    UInt32  type;
    UInt32  flags;
    SInt16  hours;
    SInt16  minutes;
    SInt16  seconds;
    SInt16  frames;
};
typedef struct CVSMPTETime    CVSMPTETime;

如果时间码值允许负时间值(格式描述标志字段设置了kCMTimeCodeFlag_NegTimesOK标志),则CVSMPTETime结构的分钟字段指示时间值是正还是负。 如果分钟字段的tcNegativeFlag(0x80)位被设置,则时间值为负。

5. Timecode Sample Data - 时间码采集数据

'tmcd'时间码样本数据格式 - QuickTime文件格式规范。

CMTimeCodeFormatType_TimeCode32 ('tmcd') Timecode Sample Data Format.
 
The timecode media sample data format is a big-endian signed 32-bit integer and may be interpreted into a timecode value as follows:
 
Hours
An 8-bit unsigned integer that indicates the starting number of hours.
 
Negative
A 1-bit value indicating the time’s sign. If bit is set to 1, the timecode record value is negative.
 
Minutes
A 7-bit integer that contains the starting number of minutes.
 
Seconds
An 8-bit unsigned integer indicating the starting number of seconds.
 
Frames
An 8-bit unsigned 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.

6. Creating a Timecode Media Sample - 创建一个时间码媒体采样

下面演示了创建时间码媒体样本所需的步骤。 该方法为SMPTE时间01:30:15:07(HH:MM:SS:FF)创建单个时间码媒体样本,持续整个视频轨道持续时间的30fps丢帧格式。

// this method creates a single SMPTE timecode media sample for time 01:30:15:07 (HH:MM:SS:FF)
// 30fps, drop frame format lasting the entire duration of the video track
- (CMSampleBufferRef)createTimecodeSampleBuffer
{
    CMSampleBufferRef sampleBuffer = NULL;
    CMBlockBufferRef dataBuffer = NULL;
 
    CMTimeCodeFormatDescriptionRef formatDescription = NULL;
    CVSMPTETime timecodeSample = {0};
 
    OSStatus status = noErr;
 
    timecodeSample.hours   = 1; // HH
    timecodeSample.minutes = 30; // MM
    timecodeSample.seconds = 15; // SS
    timecodeSample.frames  = 7; // FF
 
    status = CMTimeCodeFormatDescriptionCreate(kCFAllocatorDefault, kCMTimeCodeFormatType_TimeCode32, CMTimeMake(100, 2997), 30, kCMTimeCodeFlag_DropFrame | kCMTimeCodeFlag_24HourMax, NULL, &formatDescription);
 
    if ((status != noErr) || !formatDescription) {
        NSLog(@"Could not create format description");
    }
 
    // use utility function to convert CVSMPTETime time into frame number to write
    int32_t frameNumberData = frameNumber32ForTimecodeUsingFormatDescription(timecodeSample, formatDescription);
 
    status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, NULL, sizeof(int32_t), kCFAllocatorDefault, NULL, 0, sizeof(int32_t), kCMBlockBufferAssureMemoryNowFlag, &dataBuffer);
    if ((status != kCMBlockBufferNoErr) || !dataBuffer) {
        NSLog(@"Could not create block buffer");
    }
 
    status = CMBlockBufferReplaceDataBytes(&frameNumberData, dataBuffer, 0, sizeof(int32_t));
    if (status != kCMBlockBufferNoErr) {
        NSLog(@"Could not write into block buffer");
    }
 
    CMSampleTimingInfo timingInfo;
    // duration of each timecode sample is from the current frame to the next frame specified along with a timecode
    // in this case the single sample will last the entire duration of the video content
    timingInfo.duration = [[sourceVideoTrack asset] duration];
    timingInfo.decodeTimeStamp = kCMTimeInvalid;
    timingInfo.presentationTimeStamp = kCMTimeZero;
 
    size_t sizes = sizeof(int32_t);
    status = CMSampleBufferCreate(kCFAllocatorDefault, dataBuffer, true, NULL, NULL, formatDescription, 1, 1, &timingInfo, 1, &sizes, &sampleBuffer);
    if ((status != noErr) || !sampleBuffer) {
        NSLog(@"Could not create block buffer");
    }
 
    CFRelease(formatDescription);
    CFRelease(dataBuffer);
 
    return sampleBuffer;
}

7. Appending a Timecode Media Sample - 追加一个时间码媒体采样

追加时间码媒体样本的方式与其他媒体数据完全相同。AVAssetWriterInput - (BOOL)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer方法用于追加打包为CMSampleBuffer对象的媒体样本。 下面代码显示了用于附加上面代码中创建的时间代码样本缓冲区的一些标准AVFoundation代码。

...
 
if ([timecodeInput isReadyForMoreMediaData] && !completedOrFailed) {
    CMSampleBufferRef sampleBuffer = NULL;
 
    sampleBuffer = [timecodeSampleBufferGenerator createTimecodeSampleBuffer];
 
    if (sampleBuffer != NULL) {
        BOOL success = [timecodeInput appendSampleBuffer:sampleBuffer];
        CFRelease(sampleBuffer);
        sampleBuffer = NULL;
 
        completedOrFailed = !success;
    } else {
        completedOrFailed = YES;
    }
}
 
...

后记

未完,待续~~~

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

推荐阅读更多精彩内容