0%

时间和媒体的表示

本章译自Apple官方文档AVFoundation Programming Guide, 是AVFoundation系列译文终篇, 介绍了AVFoundation框架中时间和媒体的表示方法, 全部译文参见我的GitBook: AVFoundation编程指南.

AV Foundation框架中用 AVAsset 表示基于时间的视听数据, 比如电影文件或视频流. AVAsset 的结构决定了AV Foundation框架大部分的工作方式. AV Foundation框架中使用的一些用来代表时间和媒体的底层数据结构来源于 Core Media 框架.

Asset的表示

AVAsset 是AV Foundation框架的核心关键类, 它提供了对视听数据的格式无关的抽象. 类之间的关系如下图所示. 大部分情况下, 使用的都是这些类的子类: 使用composition的子类创建新的asset, 使用AVURLAsset根据一个指定的URL创建asset.

AVAsset provides an abstraction of time-based audiovisual data

一个asset包含一组track, 每个track都有特定媒体类型, 包括但不限于audio, video, text, closed captions 以及subtitles. Asset对象提供整个资源的信息, 比如时长和标题. Asset对象也可能包含元数据(metadata), metadata由AVMetadataItem类表示.

如下图所示, 一个track由AVAssetTrack类表示. 简单场景下, 一个track代表audio component, 另一个track代表video component; 复杂场景下, 可能有多个audio 和 video 重叠的track.

AVAssetTrack

一个track包含多个属性, 比如类型(video or audio), 视觉或听觉特性, 元数据, 以及时间轴(表现在其父asset中). 此外, track还包含一个描述格式的数组. 这个数组中的元素为CMFormatDescription对象(参见CMFormatDescriptionRef), 用来描述track包含的媒体格式信息.

一个track可能被分为几段, 每一段由一个AVAssetTrackSegment对象表示. 一个AVAssetTrackSegment对象就是一个由资源数据到track时间轴的映射.

时间的表示

AV Foundation框架中的时间由一个Core Media框架中的数据结构表示.

用CMTime表示一段时间

CMTime是一个以有理数表示时间的C语言结构体, 用一个int64_t类型作为分子, 一个int32_t类型作为分母. 从概念上来看, 时间段(timescale)描述了一秒中包含多少个时间单元. 如果timescale等于4, 则每个时间单元代表四分之一秒; 果timescale等于10, 则每个时间单元代表十分之一秒, 以此类推.

除了用来表示时间, CMTime还可以用来表示非数值的值: 正无穷大, 负无穷大, 不确定.

使用CMTime

使用方法CMTimeMake或者CMTimeMakeWithSeconds创建一个时间.

CMTime time1 = CMTimeMake(200, 2); // 200 half-seconds
CMTime time2 = CMTimeMake(400, 4); // 400 quarter-seconds
 
// time1 and time2 both represent 100 seconds, but using different timescales.
if (CMTimeCompare(time1, time2) == 0) {
    NSLog(@"time1 and time2 are the same");
}
 
Float64 float64Seconds = 200.0 / 3;
CMTime time3 = CMTimeMakeWithSeconds(float64Seconds , 3); // 66.66... third-seconds
time3 = CMTimeMultiply(time3, 3);
// time3 now represents 200 seconds; next subtract time1 (100 seconds).
time3 = CMTimeSubtract(time3, time1);
CMTimeShow(time3);
 
if (CMTIME_COMPARE_INLINE(time2, ==, time3)) {
    NSLog(@"time2 and time3 are the same");
}

更多详细信息参见CMTime Reference.

CMTime的特殊值

Core Media框架提供了一些常量: kCMTimeZero, kCMTimeInvalid,kCMTimePositiveInfinitykCMTimeNegativeInfinity. CMTime结构体能够进行很多操作, 比如要判断一个时间是否有效, 可以使用一些定义好的宏, 例如CMTIME_IS_INVALID, CMTIME_IS_POSITIVE_INFINITY 或者 CMTIME_IS_INDEFINITE.

CMTime myTime = <#Get a CMTime#>;
if (CMTIME_IS_INVALID(myTime)) {
    // Perhaps treat this as an error; display a suitable alert to the user.
}

不能将CMTime结构体与kCMTimeZero直接进行比较.

将CMTime转换为对象

如果要在注释或者Core Foundation容器中使用CMTime, 使用方法CMTimeCopyAsDictionaryCMTimeMakeFromDictionary 可以在CMTime结构体和CFDictionary类型(参见CFDictionaryRef)之间进行相互转换. 使用方法CMTimeCopyDescription可以获取CMTime结构体的字符串描述.

循环次数(Epochs)

CMTime结构体中的epoch通常被设置为0. 但是你可以使用这个值来区分不同循环次数中的同一个时间点.

用CMTimeRange表示一个时间范围

CMTimeRange是一个C语言结构体, 包含两个CMTime类型的属性: 起始时间start和时长duration. 一个时间范围并不包含start加上duration得到的时间.

使用方法CMTimeRangeMakeCMTimeRangeFromTimeToTime 创建一个时间范围, 但是存在一些限制:

  • CMTimeRange不能跨过epoch
  • 只能对startepoch值相同的CMTimeRange进行相互操作
  • durationepoch值应该一直为0, startepoch值为非负

处理CMTimeRange

Core Media框架提供了一个时间范围是否包含某个时间点或者其他时间范围的方法, 以及判断两个时间范围是否相同, 对两个时间范围进行交集和并集运算的方法. 例如, CMTimeRangeContainsTime, CMTimeRangeEqual, CMTimeRangeContainsTimeRangeCMTimeRangeGetUnion.

注意下面的表达式永远返回false:

CMTimeRangeContainsTime(range, CMTimeRangeGetEnd(range))

更多相关的详细信息, 参见CMTimeRange Reference.

CMTimeRange的特殊值

Core Media提供了一个表示空范围的常量和一个表示无效范围的常量: kCMTimeRangeZerokCMTimeRangeInvalid. 可以使用以下这些宏对CMTimeRange的特殊值进行判断: CMTIMERANGE_IS_VALID, CMTIMERANGE_IS_INVALID, CMTIMERANGE_IS_EMPTY, CMTIMERANGE_IS_EMPTY.

CMTimeRange myTimeRange = <#Get a CMTimeRange#>;
if (CMTIMERANGE_IS_EMPTY(myTimeRange)) {
    // The time range is zero.
}

不能将CMTimeRange结构体与kCMTimeRangeInvalid直接进行比较.

将CMTimeRange转换为对象

如果要在注释或者Core Foundation容器中使用CMTimeRange, 使用方法CMTimeRangeCopyAsDictionaryCMTimeRangeMakeFromDictionary 可以在CMTimeRange结构体和CFDictionary类型(参见CFDictionaryRef)之间进行相互转换. 使用方法CMTimeRangeCopyDescription可以获取CMTimeRange结构体的字符串描述.

媒体的表示

视频数据和与其相关联的元数据都使用Core Media框架中的对象类型来表示. Core Media使用CMSampleBuffer(参见CMSampleBufferRef)类型表示视频数据. 一个CMSampleBuffer对象是一个包含了视频数据帧的sample buffer, 用来作为 Core Video pixel buffer(参见CVPixelBufferRef). 可以使用CMSampleBufferGetImageBuffer方法访问sample buffer中的pixel buffer.

CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(<#A CMSampleBuffer#>);

可以在 pixel buffer 访问到实际的视频数据, 参见下文.

此外, 对于视频数据而言, 还可以视频帧其他方面的信息:

  • 时间信息: 使用CMSampleBufferGetPresentationTimeStampCMSampleBufferGetDecodeTimeStamp可以分别获取视频帧的初始时间和解码时间

  • 格式信息: 包含在一个CMFormatDescription对象中(参见CMFormatDescriptionRef). 通过格式描述, 可以使用CMVideoFormatDescriptionGetCodecType获取视频的编码信息, 使用CMVideoFormatDescriptionGetDimensions获取视频尺寸

  • 元数据: 以附件形式存储在一个字典中, 通过CMGetAttachment获取:

    CMSampleBufferRef sampleBuffer = <#Get a sample buffer#>;
    CFDictionaryRef metadataDictionary =
    CMGetAttachment(sampleBuffer, CFSTR(“MetadataDictionary”, NULL);
    if (metadataDictionary) {
    // Do something with the metadata.
    }

将CMSampleBuffer转换为UIImage

下面的代码示例了如何将CMSampleBuffer转换为UIImage. 这个转换相当消耗性能, 使用时必须进行谨慎考虑.

- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer
{
    // Get a CMSampleBuffer's Core Video image buffer for the media data
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // Lock the base address of the pixel buffer
    CVPixelBufferLockBaseAddress(imageBuffer, 0);
 
    // Get the number of bytes per row for the pixel buffer
    void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
 
    // Get the number of bytes per row for the pixel buffer
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    // Get the pixel buffer width and height
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);
 
    // Create a device-dependent RGB color space
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
 
    // Create a bitmap graphics context with the sample buffer data
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
      bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    // Create a Quartz image from the pixel data in the bitmap graphics context
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    // Unlock the pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);
 
    // Free up the context and color space
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
 
    // Create an image object from the Quartz image
    UIImage *image = [UIImage imageWithCGImage:quartzImage];
 
    // Release the Quartz image
    CGImageRelease(quartzImage);
 
    return (image);
}