1、实现原理


  • 采集: 采集视频帧使用AVCaptureSession,采集音频帧使用Audio Unit

  • 编码: 编码视频数据使用VideoToolbox中vtCompresssion硬编,编码音频数据使用audio converter软编.

  • 同步: 根据时间戳生成策略

  • 合成: 使用FFmpeg mux编码的音视频数据以合成视频流

  • 后续: 合成好的视频流可以通过网络传输或是录制成文件

2、阅读前提


  • 音视频基础知识

  • 推荐必读:H264, H265硬件编解码基础及码流分析

  • iOS视频采集实战(AVCaptureSession)

  • Audio Unit采集音频实战

  • 视频编码实战

  • 音频编码实战

  • iOS FFmpeg环境搭建

代码地址 : iOS完整推流

掘金地址 : iOS完整推流

简书地址 : iOS完整推流

博客地址 : iOS完整推流

3、总体架构


1.mux

对于iOS而言,我们可以通过底层API捕获视频帧与音频帧数据,捕获视频帧使用AVFoundation框架中的AVCaptureSession, 其实它同时也可以捕获音频数据,而因为我们想使用最低延时与最高音质的音频, 所以需要借助最底层的音频捕捉框架Audio Unit,然后使用VideoToolbox框架中的VTCompressionSessionRef可以对视频数据进行编码,使用AudioConverter可以对音频数据进行编码,我们在采集时可以将第一帧I帧产生时的系统时间作为音视频时间戳的一个起点,往后的视频说都基于此,由此可扩展做音视频同步方案,最终,我们将比那编码好的音视频数据通过FFmpeg进行合成,这里以asf流为例进行合成,并将生成好的asf流写入文件,以供测试. 生成好的asf流可直接用于网络传输.

3.1 简易流程

采集视频

  • 创建AVCaptureSession对象

  • 指定分辨率:sessionPreset/activeFormat,指定帧率setActiveVideoMinFrameDuration/setActiveVideoMaxFrameDuration

  • 指定摄像头位置:AVCaptureDevice

  • 指定相机其他属性: 曝光,对焦,闪光灯,手电筒等等...

  • 将摄像头数据源加入session

  • 指定采集视频的格式:yuv,rgb....kCVPixelBufferPixelFormatTypeKey

  • 将输出源加入session

  • 创建接收视频帧队列:- (void)setSampleBufferDelegate:(nullable id<AVCaptureVideoDataOutputSampleBufferDelegate>)sampleBufferDelegate queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue

  • 将采集视频数据渲染到屏幕:AVCaptureVideoPreviewLayer

  • 在回调函数中获取视频帧数据: CMSampleBufferRef

采集音频

  • 配置音频格式ASBD: 采样率,声道数,采样位数,数据精度,每个包中字节数等等...

  • 设置采样时间: setPreferredIOBufferDuration

  • 创建audio unit对象,指定分类. AudioComponentInstanceNew

  • 设置audio unit属性: 打开输入,禁止输出...

  • 为接收的音频数据分配大小kAudioUnitProperty_ShouldAllocateBuffer

  • 设置接收数据的回调

  • 开始audio unit: AudioOutputUnitStart

  • 在回调函数中获取音频数据: AudioUnitRender

编码视频数据

  • 指定编码器宽高类型回调并创建上下文对象: VTCompressionSessionCreate

  • 设置编码器属性:缓存帧数, 帧率, 平均码率, 最大码率, 实时编码, 是否重排序, 配置信息, 编码模式, I帧间隔时间等.

  • 准备编码数据: VTCompressionSessionPrepareToEncodeFrames

  • 开始编码: VTCompressionSessionEncodeFrame

  • 回调函数中获取编码后的数据CMBlockBufferRef

  • 根据合成码流格式,这里是asf所以需要Annex B格式,自己组装sps,pps,start code.

编码音频数据

  • 提供原始数据类型与编码后数据类型的ASBD

  • 指定编码器类型kAudioEncoderComponentType

  • 创建编码器AudioConverterNewSpecific

  • 设置编码器属性: 比特率, 编码质量等

  • 将1024个采样点原始PCM数据传入编码器

  • 开始编码: AudioConverterFillComplexBuffer

  • 获取编码后的AAC数据

音视频同步

以编码的第一帧视频的系统时间作为音视频数据的基准时间戳,随后将采集到音视频数据中的时间戳减去该基准时间戳作为各自的时间戳, 同步有两种策略,一种是以音频时间戳为准, 即当出现错误时,让视频时间戳去追音频时间戳,这样做即会造成看到画面会快进或快退,二是以视频时间戳为准,即当出现错误时,让音频时间戳去追视时间戳,即声音可能会刺耳,不推荐.所以一般使用第一种方案,通过估计下一帧视频时间戳看看如果超出同步范围则进行同步.

FFmpeg合成数据流

  • 初始化FFmpeg相关参数: AVFormatContext (管理合成上下文), AVOutputFormat(合成流格式), AVStream(音视频数据流)...

  • 创建上下文对象AVFormatContext: avformat_alloc_context

  • 根据数据类型生成编码器AVCodec: avcodec_find_encoder 视频:AV_CODEC_ID_H264/AV_CODEC_ID_HEVC,音频:AV_CODEC_ID_AAC

  • 生成流AVStream: avformat_new_stream

  • 指定音视频流中各个参数信息, 如数据格式,视频宽高帧率,比特率,基准时间,extra data, 音频:采样率,声道数, 采样位数等等.

  • 指定上下文及流格式中的音视频编码器id: video_codec_id, audio_codec_id

  • 生成视频流头数据: 当音视频编码器都填充到上下文对象后,即可生产该类型对应的头信息, 此头信息作为解码音视频数据的重要信息,一定需要正确合成.avformat_write_header

  • 将音视频数据装入动态数组中.

  • 合成音视频数据: 通过另一条线程取出动态数组中的音视频数据,通过比较时间戳的方式进行同步合成.

  • 将音视频数据装入AVPacket中

  • 产生合成的数据av_write_frame

3.2 文件结构

2.file

3.3 快速使用

  • 初始化相关模块

- (void)viewDidLoad {

[superviewDidLoad];

// Do any additional setup after loading the view.

[selfconfigureCamera];

[selfconfigureAudioCapture];

[selfconfigureAudioEncoder];

[selfconfigurevideoEncoder];

[selfconfigureAVMuxHandler];

[selfconfigureAVRecorder];

}

  • 在相机回调中将原始yuv数据送去编码

- (void)xdxCaptureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {

if ([output isKindOfClass:[AVCaptureVideoDataOutput class]] == YES) {

if (self.videoEncoder) {

[self.videoEncoder startEncodeDataWithBuffer:sampleBuffer

isNeedFreeBuffer:NO];

}

}

}

  • 通过回调函数接收编码后的视频数据并将其送给合成流类.

#pragma mark Video Encoder

- (void)receiveVideoEncoderData:(XDXVideEncoderDataRef)dataRef {

[self.muxHandleraddVideoData:dataRef->datasize:(int)dataRef->sizetimestamp:dataRef->timestampisKeyFrame:dataRef->isKeyFrameisExtraData:dataRef->isExtraDatavideoFormat:XDXMuxVideoFormatH264];

}

  • 在采集音频回调中接收音频数据并编码,最终将编码数据也送入合成流类

#pragma mark Audio Capture and Audio Encode

- (void)receiveAudioDataByDevice:(XDXCaptureAudioDataRef)audioDataRef {

[self.audioEncoderencodeAudioWithSourceBuffer:audioDataRef->data

sourceBufferSize:audioDataRef->size

pts:audioDataRef->pts

completeHandler:^(XDXAudioEncderDataRefdataRef) {

if (dataRef->size>10) {

[self.muxHandleraddAudioData:(uint8_t*)dataRef->data

size:dataRef->size

channelNum:1

sampleRate:44100

timestamp:dataRef->pts];

}

free(dataRef->data);

}];

}

  • 先写文件后,随后接收合成后的数据并写入文件.

#pragma mark Mux

- (IBAction)startRecordBtnDidClicked:(id)sender {

int size = 0;

char *data = (char *)[self.muxHandler getAVStreamHeadWithSize:&size];

[self.recorder startRecordWithIsHead:YES data:data size:size];

self.isRecording = YES;

}

- (void)receiveAVStreamWithIsHead:(BOOL)isHead data:(uint8_t *)data size:(int)size {

if (isHead) {

return;

}

if (self.isRecording) {

[self.recorder startRecordWithIsHead:NO data:(char *)data size:size];

}

}

4、具体实现


本例中音视频采集编码模块在前面文章中已经详细介绍,这里不再重复,如需帮助请参考上文的阅读前提.下面仅介绍合成流.

4.1 初始化FFmpeg相关对象.

  • AVFormatContext: 管理合成流上下文对象

  • AVOutputFormat: 合成流的格式,这里使用的asf数据流

  • AVStream: 音视频数据流具体信息

- (void)configureFFmpegWithFormat:(const char *)format {

if(m_outputContext != NULL) {

av_free(m_outputContext);

m_outputContext = NULL;

}

m_outputContext = avformat_alloc_context();

m_outputFormat = av_guess_format(format, NULL, NULL);

m_outputContext->oformat = m_outputFormat;

m_outputFormat->audio_codec = AV_CODEC_ID_NONE;

m_outputFormat->video_codec = AV_CODEC_ID_NONE;

m_outputContext->nb_streams = 0;

m_video_stream = avformat_new_stream(m_outputContext, NULL);

m_video_stream->id = 0;

m_audio_stream = avformat_new_stream(m_outputContext, NULL);

m_audio_stream->id = 1;

log4cplus_info(kModuleName, "configure ffmpeg finish.");

}

4.2 配置视频流的详细信息

设置该编码的视频流中详细的信息, 如编码器类型,配置信息,原始视频数据格式,视频的宽高,比特率,帧率,基准时间戳,extra data等.

这里最重要的就是extra data,注意,因为我们要根据extra data才能生成正确的头数据,而asf流需要的是annux b格式的数据,苹果采集的视频数据格式为avcc所以在编码模块中已经将其转为annux b格式的数据,并通过参数传入,这里可以直接使用,关于这两种格式区别也可以参考阅读前提中的码流介绍的文章.

- (void)configureVideoStreamWithVideoFormat:(XDXMuxVideoFormat)videoFormatextraData:(uint8_t*)extraDataextraDataSize:(int)extraDataSize {

if (m_outputContext==NULL) {

log4cplus_error(kModuleName, "%s: m_outputContext is null",__func__);

return;

}

if(m_outputFormat==NULL){

log4cplus_error(kModuleName, "%s: m_outputFormat is null",__func__);

return;

}

AVFormatContext*formatContext=avformat_alloc_context();

AVStream*stream=NULL;

if(XDXMuxVideoFormatH264==videoFormat) {

AVCodec*codec=avcodec_find_encoder(AV_CODEC_ID_H264);

stream=avformat_new_stream(formatContext, codec);

stream->codecpar->codec_id=AV_CODEC_ID_H264;

}elseif(XDXMuxVideoFormatH265==videoFormat) {

AVCodec*codec=avcodec_find_encoder(AV_CODEC_ID_HEVC);

stream=avformat_new_stream(formatContext, codec);

stream->codecpar->codec_tag =MKTAG('h', 'e', 'v', 'c');

stream->codecpar->profile =FF_PROFILE_HEVC_MAIN;

stream->codecpar->format =AV_PIX_FMT_YUV420P;

stream->codecpar->codec_id =AV_CODEC_ID_HEVC;

}

stream->codecpar->format =AV_PIX_FMT_YUVJ420P;

stream->codecpar->codec_type =AVMEDIA_TYPE_VIDEO;

stream->codecpar->width =1280;

stream->codecpar->height =720;

stream->codecpar->bit_rate =1024*1024;

stream->time_base.den =1000;

stream->time_base.num =1;

stream->time_base = (AVRational){1, 1000};

stream->codec->flags |=AV_CODEC_FLAG_GLOBAL_HEADER;

memcpy(m_video_stream, stream, sizeof(AVStream));

if(extraData) {

intnewExtraDataSize=extraDataSize+AV_INPUT_BUFFER_PADDING_SIZE;

m_video_stream->codecpar->extradata_size=extraDataSize;

m_video_stream->codecpar->extradata = (uint8_t*)av_mallocz(newExtraDataSize);

memcpy(m_video_stream->codecpar->extradata, extraData, extraDataSize);

}

av_free(stream);

m_outputContext->video_codec_id=m_video_stream->codecpar->codec_id;

m_outputFormat->video_codec =m_video_stream->codecpar->codec_id;

self.isReadyForVideo=YES;

[selfproductStreamHead];

}

4.3 配置音频流的详细信息

首先根据编码音频的类型生成编码器并生成流对象,然后 配置音频流的详细信息,如压缩数据格式,采样率,声道数,比特率,extra data等等.这里要注意的是extra data是为了保存mp4文件时播放器能够正确解码播放准备的,可以参考这几篇文章:audio extra data1,audio extra data2

- (void)configureAudioStreamWithChannelNum:(int)channelNumsampleRate:(int)sampleRate {

AVFormatContext*formatContext =avformat_alloc_context();

AVCodec *codec =avcodec_find_encoder(AV_CODEC_ID_AAC);

AVStream *stream =avformat_new_stream(formatContext, codec);

stream->index =1;

stream->id =1;

stream->duration =0;

stream->time_base.num=1;

stream->time_base.den=1000;

stream->start_time =0;

stream->priv_data =NULL;

stream->codecpar->codec_type =AVMEDIA_TYPE_AUDIO;

stream->codecpar->codec_id =AV_CODEC_ID_AAC;

stream->codecpar->format =AV_SAMPLE_FMT_S16;

stream->codecpar->sample_rate =sampleRate;

stream->codecpar->channels =channelNum;

stream->codecpar->bit_rate =0;

stream->codecpar->extradata_size=2;

stream->codecpar->extradata = (uint8_t*)malloc(2);

stream->time_base.den =25;

stream->time_base.num =1;

/*

* why we put extra data here for audio: when save to MP4 file, the player can not decode it correctly

* http://ffmpeg-users.933282.n4.nabble.com/AAC-decoder-td1013071.html

* http://ffmpeg.org/doxygen/trunk/mpeg4audio_8c.html#aa654ec3126f37f3b8faceae3b92df50e

* extra data have 16 bits:

* Audio object type - normally 5 bits, but 11 bits if AOT_ESCAPE

* Sampling index - 4 bits

* if (Sampling index == 15)

* Sample rate - 24 bits

* Channel configuration - 4 bits

* last reserved- 3 bits

* for exmpale: "Low Complexity Sampling frequency 44100Hz, 1 channel mono":

* AOT_LC == 2 -> 00010

- * 44.1kHz == 4 -> 0100

+ * 44.1kHz == 4 -> 0100 48kHz == 3 -> 0011

* mono == 1 -> 0001

* so extra data: 00010 0100 0001 000 ->0x12 0x8

+ 00010 0011 0001 000 ->0x11 0x88

+

*/

if (stream->codecpar->sample_rate==44100) {

stream->codecpar->extradata[0] =0x12;

//iRig mic HD have two chanel 0x11

if(channelNum==1)

stream->codecpar->extradata[1] =0x8;

else

stream->codecpar->extradata[1] =0x10;

}elseif (stream->codecpar->sample_rate==48000) {

stream->codecpar->extradata[0] =0x11;

//iRig mic HD have two chanel 0x11

if(channelNum==1)

stream->codecpar->extradata[1] =0x88;

else

stream->codecpar->extradata[1] =0x90;

}elseif (stream->codecpar->sample_rate==32000){

stream->codecpar->extradata[0] =0x12;

if (channelNum==1)

stream->codecpar->extradata[1] =0x88;

else

stream->codecpar->extradata[1] =0x90;

}

elseif (stream->codecpar->sample_rate==16000){

stream->codecpar->extradata[0] =0x14;

if (channelNum==1)

stream->codecpar->extradata[1] =0x8;

else

stream->codecpar->extradata[1] =0x10;

}elseif(stream->codecpar->sample_rate==8000){

stream->codecpar->extradata[0] =0x15;

if (channelNum==1)

stream->codecpar->extradata[1] =0x88;

else

stream->codecpar->extradata[1] =0x90;

}

stream->codec->flags|=AV_CODEC_FLAG_GLOBAL_HEADER;

memcpy(m_audio_stream, stream, sizeof(AVStream));

av_free(stream);

m_outputContext->audio_codec_id=stream->codecpar->codec_id;

m_outputFormat->audio_codec =stream->codecpar->codec_id;

self.isReadyForAudio=YES;

[selfproductStreamHead];

}

4.4 生成流头数据

当前面2,3部都配置完成后,我们将音视频流注入上下文对象及对象中的流格式中,即可开始生成头数据.avformat_write_header

- (void)productStreamHead {

log4cplus_debug("record", "%s,line:%d",__func__,__LINE__);

if (m_outputFormat->video_codec == AV_CODEC_ID_NONE) {

log4cplus_error(kModuleName, "%s: video codec is NULL.",__func__);

return;

}

if(m_outputFormat->audio_codec == AV_CODEC_ID_NONE) {

log4cplus_error(kModuleName, "%s: audio codec is NULL.",__func__);

return;

}

/* prepare header and save header data in a stream */

if (avio_open_dyn_buf(&m_outputContext->pb) < 0) {

avio_close_dyn_buf(m_outputContext->pb, NULL);

log4cplus_error(kModuleName, "%s: AVFormat_HTTP_FF_OPEN_DYURL_ERROR.",__func__);

return;

}

/*

* HACK to avoid mpeg ps muxer to spit many underflow errors

* Default value from FFmpeg

* Try to set it use configuration option

*/

m_outputContext->max_delay = (int)(0.7*AV_TIME_BASE);

int result = avformat_write_header(m_outputContext,NULL);

if (result < 0) {

log4cplus_error(kModuleName, "%s: Error writing output header, res:%d",__func__,result);

return;

}

uint8_t * output = NULL;

int len = avio_close_dyn_buf(m_outputContext->pb, (uint8_t **)(&output));

if(len > 0 && output != NULL) {

av_free(output);

self.isReadyForHead = YES;

if (m_avhead_data) {

free(m_avhead_data);

}

m_avhead_data_size = len;

m_avhead_data = (uint8_t *)malloc(len);

memcpy(m_avhead_data, output, len);

if ([self.delegate respondsToSelector:@selector(receiveAVStreamWithIsHead:data:size:)]) {

[self.delegate receiveAVStreamWithIsHead:YES data:output size:len];

}

log4cplus_error(kModuleName, "%s: create head length = %d",__func__, len);

}else{

self.isReadyForHead = NO;

log4cplus_error(kModuleName, "%s: product stream header failed.",__func__);

}

}

4.5 然后将传来的音视频数据装入数组中

该数组通过封装C++中的vector实现一个轻量级数据结构以缓存数据.

4.6 合成音视频数据

新建一条线程专门合成音视频数据,合成策略即取出音视频数据中时间戳较小的一帧先写,因为音视频数据总体偏差不大,所以理想情况应该是取一帧视频,一帧音频,当然因为音频采样较快,可能会相对多一两帧,而当音视频数据由于某种原因不同步时,则会等待,直至时间戳重新同步才能继续进行合成.

interr=pthread_create(&m_muxThread,NULL,MuxAVPacket,(__bridge_retainedvoid*)self);

if(err!=0){

log4cplus_error(kModuleName, "%s: create thread failed: %s",__func__, strerror(err));

}

void*MuxAVPacket(void*arg) {

pthread_setname_np("XDX_MUX_THREAD");

XDXAVStreamMuxHandler*instance= (__bridge_transferXDXAVStreamMuxHandler*)arg;

if(instance!=nil) {

[instancedispatchAVData];

}

returnNULL;

}

#pragma mark Mux

- (void)dispatchAVData {

XDXMuxMediaListaudioPack;

XDXMuxMediaListvideoPack;

memset(&audioPack, 0, sizeof(XDXMuxMediaList));

memset(&videoPack, 0, sizeof(XDXMuxMediaList));

[m_AudioListPackreset];

[m_VideoListPackreset];

while (true) {

intvideoCount= [m_VideoListPackcount];

intaudioCount= [m_AudioListPackcount];

if(videoCount==0||audioCount==0) {

usleep(5*1000);

log4cplus_debug(kModuleName, "%s: Mux dispatch list: v:%d, a:%d",__func__,videoCount, audioCount);

continue;

}

if(audioPack.timeStamp==0) {

[m_AudioListPackpopData:&audioPack];

}

if(videoPack.timeStamp==0) {

[m_VideoListPackpopData:&videoPack];

}

if(audioPack.timeStamp>=videoPack.timeStamp) {

log4cplus_debug(kModuleName, "%s: Mux dispatch input video time stamp = %llu",__func__,videoPack.timeStamp);

if(videoPack.data!=NULL&&videoPack.data->data!=NULL){

[selfaddVideoPacket:videoPack.data

timestamp:videoPack.timeStamp

extraDataHasChanged:videoPack.extraDataHasChanged];

av_free(videoPack.data->data);

av_free(videoPack.data);

}else{

log4cplus_error(kModuleName, "%s: Mux Video AVPacket data abnormal",__func__);

}

videoPack.timeStamp=0;

}else {

log4cplus_debug(kModuleName, "%s: Mux dispatch input audio time stamp = %llu",__func__,audioPack.timeStamp);

if(audioPack.data!=NULL&&audioPack.data->data!=NULL) {

[selfaddAudioPacket:audioPack.data

timestamp:audioPack.timeStamp];

av_free(audioPack.data->data);

av_free(audioPack.data);

}else {

log4cplus_error(kModuleName, "%s: Mux audio AVPacket data abnormal",__func__);

}

audioPack.timeStamp=0;

}

}

}

4.7 获取合成好的视频流

通过av_write_frame即可获取合成好的数据.

- (void)productAVDataPacket:(AVPacket *)packet extraDataHasChanged:(BOOL)extraDataHasChanged {

BOOL isVideoIFrame = NO;

uint8_t *output = NULL;

int len = 0;

if (avio_open_dyn_buf(&m_outputContext->pb) < 0) {

return;

}

if(packet->stream_index == 0 && packet->flags != 0) {

isVideoIFrame = YES;

}

if (av_write_frame(m_outputContext, packet) < 0) {

avio_close_dyn_buf(m_outputContext->pb, (uint8_t **)(&output));

if(output != NULL)

free(output);

log4cplus_error(kModuleName, "%s: Error writing output data",__func__);

return;

}

len = avio_close_dyn_buf(m_outputContext->pb, (uint8_t **)(&output));

if(len == 0 || output == NULL) {

log4cplus_debug(kModuleName, "%s: mux len:%d or data abnormal",__func__,len);

if(output != NULL)

av_free(output);

return;

}

if ([self.delegate respondsToSelector:@selector(receiveAVStreamWithIsHead:data:size:)]) {

[self.delegate receiveAVStreamWithIsHead:NO data:output size:len];

}

if(output != NULL)

av_free(output);

}

原文https://zhuanlan.zhihu.com/p/615091766

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

iOS完整推流采集音视频数据编码同步合成流相关推荐

  1. iOS 采集音视频及写入文件

    音视频采集包括两部分:视频采集和音频采集.在iOS中可以同步采集视频与音频,通过系统框架AVFoundation,可以帮助我们采集音频与视频,对于视频还可以进行切换前后摄像头,最终我们将录制好的视频写 ...

  2. 游戏陪玩平台系统中iOS 采集音视频及写入文件的实现

    在游戏陪玩平台系统中音视频采集包括两部分:视频采集和音频采集.在iOS中可以同步采集视频与音频,通过系统框架AVFoundation,可以帮助游戏陪玩平台系统采集音频与视频,对于视频还可以进行切换前后 ...

  3. 互动协作白板与音视频实时同步技术实践

    本文整理自即构科技互动白板技术负责人陈晓聪在LiveVideoStack的线上分享,内容主要围绕白板与音视频的同步和白板的多端实时互动两个角度,深度解析即构在互动白板方面的技术探索实践. 文 / 陈晓 ...

  4. 音视频的同步原理——老文章,比较清晰

    http://blog.csdn.net/beyond_cn/article/details/20310177 1.   音视频同步的原理 2.  音视频同步的较正方法 3.  音视频同步时遇到的问题 ...

  5. IOS_音视频不同步问题

    音视频不同步 1.问题现象:iOS版本在网络环境差的情况下,会比较容易出现音视频不同步的问题 2.问题分析: 经多次测试发现,观看时间越长,音频延迟的越严重: 然后查看Log,发现音频接收缓存队列中并 ...

  6. 用FFMPEG SDK进行视频转码压缩时解决音视频不同步问题的方法(转) PTS DTS

    用FFMPEG SDK进行视频转码压缩的时候,转码成功后去看视频的内容,发现音视频是不同步的.这个的确是一个恼火的事情.我在用FFMPEG SDK做h264格式的FLV文件编码Filter的时候就碰到 ...

  7. python中使用ffmpeg进行视频指定时长截断(解决剪切后音视频不同步的问题)

    1. 获取视频时长 有两种方式获取视频总时长: python-ffmpeg( 获取的是视频总秒数 ) video_time = float(ffmpeg.probe(video)['format'][ ...

  8. php音视频同步,FFMPEG转码音视频不同步情况总结

    [导读]使用FFMPEG转码一年半的时间,遇到了各种情况的音视频不同步,下面我们就来根据问题出现的原因,对所遇到的音视频不同步做一个分类. 使用FFMPEG转码一年半的时间,遇到了各种情况的音视频不同 ...

  9. iOS WebRTC多人音视频建立的流程

    前言 本文主要以"代码是最好的注释"为基点,介绍在处理iOS端多人音视频的建立流程. 在看本篇前建议先了解一下多人音视频通讯现在的常用架构,参考<WebRTC多人音视频聊天架 ...

最新文章

  1. HALCON标定倾斜安装镜头
  2. java 读utf-8 xml_〖JAVA经验〗JDom输出UTF-8的XML完美解决方法
  3. sendmail邮件服务器配置
  4. 有关identity的小技巧
  5. (译) JSON-RPC 2.0 规范(中文版)
  6. mysql列属性auto(mysql笔记四)
  7. SQL SERVER2014 安装 Error code 0x858C001B.
  8. ThreadLocal内存溢出代码演示和原因分析!
  9. linux之expect
  10. Fragment与Acitvity通信
  11. 函数嵌套和nonlocal声明
  12. MAC OS下使用JAVE将amr转mp3的坑
  13. 【业务知识】金融、银行业务知识点(转载)
  14. SpringBoot 定时任务实现数据同步方法
  15. ios 音高测试软件,‎App Store 上的“固定音高训练”
  16. 通孔的作用是什么linux,转载:PCB名詞解釋:通孔、盲孔、埋孔
  17. 我辛辛苦苦做了一个月的项目,组长年底用来写了晋升PPT
  18. canvas 花蕊绽放
  19. 04,Android 单位mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi
  20. Aspose.Words for .NET使用表格教程之水平和垂直合并表格中单元格

热门文章

  1. 成都农业科技职业学院计算机专业好不好,成都农业科技职业学院
  2. origin python控制台怎么用_python代码运行助手如何使用
  3. java入门之参考书
  4. IntelliJ Idea如何将设置tab设置为4个空格
  5. 金融工程学(二):远期与期货
  6. url-pattern标签
  7. 打动我的央视一套黄金剧场《牵挂》_huadingjin_新浪博客
  8. lol无法启动此程序因为计算机丢失,Win7系统玩英雄联盟lol提示计算机丢失auncher.dll如何解决...
  9. Mathtype如何打出偏导、一撇
  10. 【附源码】Java计算机毕业设计小区宠物管理系统(程序+LW+部署)