在 iOS 上捕获/分割视频并通过 HLS 重新加入会导致音频丢失

Posted

技术标签:

【中文标题】在 iOS 上捕获/分割视频并通过 HLS 重新加入会导致音频丢失【英文标题】:Capturing/segmenting video on iOS and rejoining via HLS results in audio dropouts 【发布时间】:2013-12-09 21:32:29 【问题描述】:

我正在尝试在 iPhone 5 上捕捉视频以进行实时上传和 HLS 流式传输。我正处于在设备上生成视频的阶段(尚未上传到服务器)。就像 SO 建议的这些链接一样,我已经编写了一些代码,每五秒钟切换一次 AssetWriter。

Upload live streaming video from iPhone like Ustream or Qik streaming video FROM an iPhone Data corruption when reading realtime H.264 output from AVAssetWriter

现在在开发期间,我只是将文件保存到本地设备并通过 XCode Organizer 将它们拉出。然后我运行 Apple 的 mediafilesegmenter 将它们简单地转换为 MPEG2-TS(它们已经低于 10 秒,因此没有发生实际的分段 - 我假设它们只是被转换为 TS)。我通过编辑在此过程中创建的各种索引文件(目前也是手动)来构建 m3u8。

当我将资产放在服务器上进行测试时,它们大部分都可以正确流式传输,但我可以判断何时有分段切换,因为音频短暂下降(可能还有视频,但我无法确定 -看起来不错)。对于从单个输入文件分段的典型 HLS 流,这显然不会发生。我不知道是什么原因造成的。

您可以在此处在您的 iPhone 上打开我的 HLS 流(您可以在 5 秒后听到音频下降,然后在 10 秒左右再次听到)

http://cdn.inv3ntion.com/ms/stitch/stitch.html

在我的创作过程中(无论是在设备上还是在后期处理中)是否发生了导致短暂音频丢失的事情?我认为我在 AssetWriter 切换期间不会丢弃任何 sampleBuffer(参见代码)。

- (void)writeSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType

    if (!self.isStarted) 
        return;
    

    @synchronized(self) 

        if (mediaType == AVMediaTypeVideo && !assetWriterVideoIn) 
            videoFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
            CFRetain(videoFormat);
            assetWriterVideoIn = [self addAssetWriterVideoInput:assetWriter withFormatDesc:videoFormat];
            [tracks addObject:AVMediaTypeVideo];
            return;
        

        if (mediaType == AVMediaTypeAudio && !assetWriterAudioIn) 
            audioFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
            CFRetain(audioFormat);
            assetWriterAudioIn = [self addAssetWriterAudioInput:assetWriter withFormatDesc:audioFormat];
            [tracks addObject:AVMediaTypeAudio];
            return;
        

        if (assetWriterAudioIn && assetWriterVideoIn) 
            recording = YES;
            if (assetWriter.status == AVAssetWriterStatusUnknown) 
                if ([assetWriter startWriting]) 
                    [assetWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
                    if (segmentationTimer) 
                        [self setupQueuedAssetWriter];
                        [self startSegmentationTimer];
                    
                 else 
                    [self showError:[assetWriter error]];
                
            

            if (assetWriter.status == AVAssetWriterStatusWriting) 
                if (mediaType == AVMediaTypeVideo) 
                    if (assetWriterVideoIn.readyForMoreMediaData) 
                        if (![assetWriterVideoIn appendSampleBuffer:sampleBuffer]) 
                            [self showError:[assetWriter error]];
                        
                    
                
                else if (mediaType == AVMediaTypeAudio) 
                    if (assetWriterAudioIn.readyForMoreMediaData) 
                        if (![assetWriterAudioIn appendSampleBuffer:sampleBuffer]) 
                            [self showError:[assetWriter error]];
                        
                    
                
            
        
    


- (void)setupQueuedAssetWriter

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^
        NSLog(@"Setting up queued asset writer...");
        queuedFileURL = [self nextFileURL];
        queuedAssetWriter = [[AVAssetWriter alloc] initWithURL:queuedFileURL fileType:AVFileTypeMPEG4 error:nil];
        if ([tracks objectAtIndex:0] == AVMediaTypeVideo) 
            queuedAssetWriterVideoIn = [self addAssetWriterVideoInput:queuedAssetWriter withFormatDesc:videoFormat];
            queuedAssetWriterAudioIn = [self addAssetWriterAudioInput:queuedAssetWriter withFormatDesc:audioFormat];
         else 
            queuedAssetWriterAudioIn = [self addAssetWriterAudioInput:queuedAssetWriter withFormatDesc:audioFormat];
            queuedAssetWriterVideoIn = [self addAssetWriterVideoInput:queuedAssetWriter withFormatDesc:videoFormat];
        
    );


- (void)doSegmentation

    NSLog(@"Segmenting...");
    AVAssetWriter *writer = assetWriter;
    AVAssetWriterInput *audioIn = assetWriterAudioIn;
    AVAssetWriterInput *videoIn = assetWriterVideoIn;
    NSURL *fileURL = currentFileURL;

    //[avCaptureSession beginConfiguration];
    @synchronized(self) 
        assetWriter = queuedAssetWriter;
        assetWriterAudioIn = queuedAssetWriterAudioIn;
        assetWriterVideoIn = queuedAssetWriterVideoIn;
    
    //[avCaptureSession commitConfiguration];
    currentFileURL = queuedFileURL;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^
        [audioIn markAsFinished];
        [videoIn markAsFinished];
        [writer finishWritingWithCompletionHandler:^
            if (writer.status == AVAssetWriterStatusCompleted ) 
                [fileURLs addObject:fileURL];
             else 
                NSLog(@"...WARNING: could not close segment");
            
        ];
    );

【问题讨论】:

【参考方案1】:

您可以尝试在 m3u8 中的每个段之间插入 #EXT-X-DISCONTINUITY,但我怀疑这是否可行。这里有很多事情可能会出错。

假设您是 44100kHz 的音频样本,每 22 微秒就有一个新的音频样本。在您关闭和重新打开文件期间,您肯定会丢失样本。如果你连接最终的波形,由于这种损失,它会比实时播放稍微快一点。实际上,这可能不是问题。

正如@vipw 所说,您还会遇到时间戳问题。每次你开始一个新的 mp4 时,你都是从时间戳零开始的。因此,玩家会感到困惑,因为时间戳会不断重置。

还有,是传输流格式。 TS 将每一帧封装成“流”。 HLS 通常有 4 个(PAT、PMT、音频和视频),每个流被分成 188 字节的数据包,带有 4 字节的标头。标头有一个每个流连续性计数器 4 位,溢出时环绕。因此,在每个 mp4 上运行 mediafilesegmenter,您通过将连续性计数器重置为零来中断每个片段的流。

您需要一个工具来接受 mp4 并创建一个流输出来维护/重写时间戳(PTS、DTS、CTS)以及连续性计数器。

【讨论】:

谢谢,是的,这开始有点意思了。基本上,每次我启动一个新的 AVAssetWriter 时,它都必须确保文件是完全自包含和独立的所以,就你而言,一切都被重置了。跟踪和重置时间戳是有道理的,我来看看。实际上,我什至不需要 DISCONTINUITY 标签,没有它们似乎也能很好地工作——我想我的片段几乎是完美的,但并不完全。 不要打死马,但不要忽略连续性计数器。它会以不可预知的方式扰乱你的流。解析器/解码器可能决定忽略数据包,直到它重新同步。关于传输流(以及 PSI 和 ES)的***文章非常好。【参考方案2】:

转移数据包

我们在使用旧版本的 ffmpeg pts 过滤器来转移数据包时遇到了麻烦。最新的 ffmpeg1 和 ffmpeg2 支持 mpegts 的时移。

这是一个 ffmpeg 调用的示例,注意 -t 表示持续时间,-initial_time 表示命令末尾的班次(继续向右滚动...) 这是一个 10 秒班次的片段

/opt/ffmpeg -i /tmp/cameo/58527/6fc2fa1a7418bf9d4aa90aa384d0eef2244631e8 -threads 0 -ss 10 -i /tmp/cameo/58527/79e684d793e209ebc9b12a5ad82298cb5e94cb54 -codec:v libx264 -pix_fmt yuv420p -preset veryfast -strict -2 -bsf:v h264_mp4toannexb -flags -global_header -crf 28 -profile:v baseline -x264opts level=3:keyint_min=24:keyint=24:scenecut=0 -b:v 100000 -bt 100000 -bufsize 100000 -maxrate 100000 -r 12 -s 320x180 -map 0:0 -map 1:0 -codec:a aac -strict -2 -b:a 64k -ab 64k -ac 2 -ar 44100 -t 9.958333333333334 -segment_time 10.958333333333334 -f segment -initial_offset 10 -segment_format mpegts -y /tmp/cameo/58527/100K%01d.ts -codec:v libx264 -pix_fmt yuv420p -preset veryfast -strict -2 -bsf:v h264_mp4toannexb -flags -global_header -crf 28 -profile:v baseline -x264opts level=3:keyint_min=24:keyint=24:scenecut=0 -b:v 200000 -bt 200000 -bufsize 200000 -maxrate 200000 -r 12 -s 320x180 -map 0:0 -map 1:0 -codec:a aac -strict -2 -b:a 64k -ab 64k -ac 2 -ar 44100 -t 9.958333333333334 -segment_time 10.958333333333334 -f segment -initial_offset 10 -segment_format mpegts -y /tmp/cameo/58527/200K%01d.ts -codec:v libx264 -pix_fmt yuv420p -preset veryfast -strict -2 -bsf:v h264_mp4toannexb -flags -global_header -crf 28 -profile:v baseline -x264opts level=3:keyint_min=24:keyint=24:scenecut=0 -b:v 364000 -bt 364000 -bufsize 364000 -maxrate 364000 -r 24 -s 320x180 -map 0:0 -map 1:0 -codec:a aac -strict -2 -b:a 64k -ab 64k -ac 2 -ar 44100 -t 9.958333333333334 -segment_time 10.958333333333334 -f segment -initial_offset 10 -segment_format mpegts -y /tmp/cameo/58527/364K%01d.ts -codec:v libx264 -pix_fmt yuv420p -preset veryfast -strict -2 -bsf:v h264_mp4toannexb -flags -global_header -crf 28 -profile:v baseline -x264opts level=3:keyint_min=24:keyint=24:scenecut=0 -b:v 664000 -bt 664000 -bufsize 664000 -maxrate 664000 -r 24 -s 480x270 -map 0:0 -map 1:0 -codec:a aac -strict -2 -b:a 64k -ab 64k -ac 2 -ar 44100 -t 9.958333333333334 -segment_time 10.958333333333334 -f segment -initial_offset 10 -segment_format mpegts -y /tmp/cameo/58527/664K%01d.ts -codec:v libx264 -pix_fmt yuv420p -preset veryfast -strict -2 -bsf:v h264_mp4toannexb -flags -global_header -crf 23 -profile:v baseline -x264opts level=3.1:keyint_min=24:keyint=24:scenecut=0 -b:v 1264000 -bt 1264000 -bufsize 1264000 -maxrate 1264000 -r 24 -s 640x360 -map 0:0 -map 1:0 -codec:a aac -strict -2 -b:a 64k -ab 64k -ac 2 -ar 44100 -t 9.958333333333334 -segment_time 10.958333333333334 -f segment -initial_offset 10 -segment_format mpegts -y /tmp/cameo/58527/1264K%01d.ts

还有我在 github 上更新的 c++ segmenter 的改编版本,但它仅针对仅视频 mpegts 进行了合理测试。 AV 仍然会导致一些问题(我不确定哪种类型的数据包应该转移到第一个视频或第一个音频数据包的新值,选择第一个视频数据包)。此外,正如您在your issue 中指出的那样,当您碰到它时,某些媒体可能会出现问题。

如果我有更多时间,我想调试您的具体案例并改进 c++ shifter。我希望上面的 ffmpeg 示例可以帮助您的 http 实时流媒体示例正常工作,我们已经解决了流媒体问题。我们目前正在解决从移位片段中出现的音频流行音乐。解决方法是在分割成分段流之前收集所有源媒体(我们可以在最终确定视频时这样做,但它会在迭代构建期间减慢我们的速度)。

【讨论】:

音频弹出正是我要解决的问题 - 听到您也遇到这种情况,这让您感到非常放心:-)。我设法使用您共享的 ffmpeg 命令的一个版本来获得更紧凑的 HLS 流(我可能不需要使用您编写的移位器,开始时间,持续时间,PTS 现在看起来都不错)。只是在分段开关处出现微小的音频下降,有时几乎无法察觉。我开始怀疑我是否可以完全摆脱它 - 也许这只是在 ios 上连续编码多个文件的症状。 我认为它是单独编码多个段的产物,而不是对来自单个源的多个段进行编码。这就是我删除pop的方式,但它仍在进行中(需要更新我们的api以接收我们最终编码的所有生成文件)【参考方案3】:

我认为您的 ts 文件不会在同一时间轴上创建。 ts 文件中包含数据包的呈现时间戳,如果在每个段上创建新的 ts,则可能存在不连续性。

您可以将记录的片段连接在一起,以便新部分在同一时间线上加盖时间戳。然后分段应该可以正常工作,并且在生成的流中分段过渡应该是平滑的。

我认为您需要一个始终保留前一段的最后一部分的过程,以便时间戳始终同步。

【讨论】:

我一直在想这些方面,但对 mp4 文件格式了解不够,不知道如何确认或修复它。我可以说我获取了原始 mp4 输出文件并将它们与 ffmpeg 连接起来,以查看输出是什么样的,并且仍然存在丢失。因此,即使我连续写入文件,每次 AVAssetWriter 开始一个新文件时似乎都会发生一些事情。 你有 ffmpeg 重写时间戳吗? 我一直在尝试 setpts 和 asetpts 但它似乎并没有覆盖现有的 pts,而是看起来它正在添加更多。 ffmpeg cli 很难,我不确定我在做正确的命令。我目前正在使用:“ffmpeg -i input.ts -vf 'setpts=572250+PTS' output.ts” input.ts 在视频流中之前有 119 个数据包,之后有 273 个数据包。不知道发生了什么。

以上是关于在 iOS 上捕获/分割视频并通过 HLS 重新加入会导致音频丢失的主要内容,如果未能解决你的问题,请参考以下文章

在 iOS 9 上播放 HLS m3u8

iOS 处理HLS视频流

在 android 设备上捕获视频并在 iOS 设备上播放

在 iOS 13 上,AVPlayer 会为此 HLS 视频选择仅音频流。我可以控制这种行为,而是让它加载视频+音频吗?

AVPlayerLayer 不在 iOS 11 中渲染离线 HLS 视频

IOS - 比特率文件视频 hls