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

时间:2013-12-09 21:32:30

标签: ios video-streaming http-live-streaming

我正在尝试在iPhone 5上捕获视频以进行实时上传和HLS流式传输。我正处于我在设备上生成视频的阶段(尚未上传到服务器)。就像SO上提到的这些链接一样,我已经将一些代码整合在一起,每隔五秒就会切换一次AssetWriters。

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

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

您可以在iPhone上打开我的HLS流(您可以在5秒后听到音频丢失并再次在10左右听到声音)

在我的创作过程中(在设备上还是在后处理过程中)会发生什么事情导致短暂的音频丢失?我不认为我在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");
            }
        }];
    });
}

3 个答案:

答案 0 :(得分:1)

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

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

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

此外,是传输流格式。 TS将每个帧封装成“流”。 HLS通常具有4(PAT,PMT,音频和视频),每个流被分成具有4字节头的188字节分组。标头有一个4位/流连续性计数器,在溢出时包裹。因此,在每个mp4上运行mediafilesegmenter,您通过将连续性计数器重新设置为零来打破每个段的流。

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

答案 1 :(得分:1)

转移数据包

我们无法使用旧版本的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 ++移位器。我希望上面的ffmpeg示例有助于让您的http直播流示例正常工作,我们已经完成了流媒体故障的分享。我们目前正在处理从移位段发生的音频弹出。修复是在分割成分段流之前收集所有源媒体(我们可以在最终确定视频时执行此操作,但在迭代构建期间它会减慢我们的速度。)

答案 2 :(得分:0)

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

可能有用的是将记录的段连接在一起,以便新的部分在同一时间轴中加上时间戳。然后,分段应该正常工作,并且段转换应该在生成的流中平滑。

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