FFmpeg+SDL2实现简易音视频同步播放器

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

SDL2文章列表

SDL系列讲解 视频渲染入门_irainsa的博客-CSDN博客

SDL 事件处理_irainsa的博客-CSDN博客

SDL 纹理渲染_irainsa的博客-CSDN博客

SDL2 PCM音频播放_irainsa的博客-CSDN博客

FFmpeg+SDL2实现视频流播放_irainsa的博客-CSDN博客

FFmpeg+SDL2实现音频流播放_irainsa的博客-CSDN博客

FFmpeg音视频同步_irainsa的博客-CSDN博客

经过前面一系列的SDL2学习,终于到最后实现一个完整的简易播放器了。

线程模型

这是实现的简易播放器的线程模型,通过这张图再结合我们之前博客中学习的内容,基本可以了解播放器的一个整体运行流程。具体代码也是根据这张图来实现。

重要结构体

VideoState

整个播放器中最重要的结构体,解复用、解码、音视频同步、渲染相关参数都在该结构体中,它贯穿了整个播放流程。

typedef struct VideoState {
    
    char filename[1024]; // 文件名称
    AVFormatContext *pFormatCtx; // 上下文
    int videoStream, audioStream; //音视频流index


    //// 同步相关
    double audio_clock;
    double frame_timer;
    double frame_last_pts;
    double frame_last_delay;

    double video_clock; 
    double video_current_pts; 
    int64_t video_current_pts_time;  

    //音频相关
    AVStream *audio_st; // 音频流
    AVCodecContext *audio_ctx; // 音频解码上下文
    PacketQueue audioq; // 音频队列
    uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3) / 2]; // 音频缓存
    unsigned int audio_buf_size;
    unsigned int audio_buf_index;
    AVFrame audio_frame; // 音频帧
    AVPacket audio_pkt; // 音频包
    uint8_t *audio_pkt_data;
    int audio_pkt_size;
    struct SwrContext *audio_swr_ctx; // 音频重采样


    //video
    AVStream *video_st; // 视频流
    AVCodecContext *video_ctx; // 视频流解码上下文
    PacketQueue videoq; // 视频流队列


    VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE]; // 解码后视频帧数组
    int pictq_size, pictq_rindex, pictq_windex;
    SDL_mutex *pictq_mutex;
    SDL_cond *pictq_cond;

    SDL_Thread *parse_tid; // 解复用线程
    SDL_Thread *video_tid;// 视频解码线程

    int quit; // 退出标记位
} VideoState;

PacketQueue

//// 解复用后音视频packet保存队列
typedef struct PacketQueue {
    AVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;

VideoPicture

//// 解码后视频帧
typedef struct VideoPicture {
    AVFrame *frame;
    int width, height;
    double pts; // 音视频同步后视频帧应该播放的时间
} VideoPicture;

具体代码

Main

  1. 初始化
  2. 创建定时器,定时视频帧的刷新
  3. 创建解复用线程
  4. 等待事件
int WinMain(int argc, char *argv[]) {
    char *file = "C:\\Users\\lenovo\\Desktop\\IMG_5950.mp4";
    SDL_Event event;
    VideoState *is;
    is = av_mallocz(sizeof(VideoState));

    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        exit(1);
    }
    //创建SDL Window
    win = SDL_CreateWindow("Media Player",
                           100,
                           100,
                           640, 480,
                           SDL_WINDOW_RESIZABLE);
    if (!win) {
        fprintf(stderr, "SDL_CreateWindow error,exit!", SDL_GetError());
        exit(1);
    }

    renderer = SDL_CreateRenderer(win, -1, 0);
    text_mutex = SDL_CreateMutex();
    
    strlcpy(is->filename, file, sizeof(is->filename));
    is->pictq_mutex = SDL_CreateMutex();
    is->pictq_cond = SDL_CreateCond();

    // 定时刷新器,主要用来控制视频的刷新
    schedule_refresh(is, 40);

    // 创建解复用线程
    is->parse_tid = SDL_CreateThread(demux_thread, "demux_thread", is);
    if (!is->parse_tid) {
        av_free(is);
        return -1;
    }


    for (;;) {
        // 等待SDL事件,否则阻塞
        SDL_WaitEvent(&event);
        switch (event.type) {
            case FF_QUIT_EVENT:
            case SDL_QUIT: // 退出
                is->quit = 1;
                goto Destroy;
            case SDL_KEYDOWN:// ESC退出
                if (event.key.keysym.sym == SDLK_ESCAPE) {
                    is->quit = 1;
                    goto Destroy;
                }
                break;
            case FF_REFRESH_EVENT: // 定时器刷新事件
                video_refresh_timer(event.user.data1);
                break;
            default:
                break;
        }
    }

    // 退出
    Destroy:
    SDL_Quit();
    return 0;

}

解复用

  1. 打开文件
  2. 找到音视频流
  3. 打开音频、视频流,创建视频解码线程,准备解码
  4. 读取packet,将音视频packet分别放入队列中,等待解码线程取出
int demux_thread(void *arg) {
    
    if ((err_code = avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)) < 0) {
        av_strerror(err_code, errors, 1024);
        return -1;
    }

    // Find the first video stream
    for (i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
            video_index < 0) {
            video_index = i;
        }
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
            audio_index < 0) {
            audio_index = i;
        }
    }
    // 打开音频流,创建解码器,配置参数
    if (audio_index >= 0) {
        stream_component_open(is, audio_index);
    }
    // 打开视频流,创建解码器,创建解码线程
    if (video_index >= 0) {
        stream_component_open(is, video_index);
        // video_tid = SDL_CreateThread(decode_video_thread, "decode_video_thread", is);
    }

    for (;;) {
        if (av_read_frame(is->pFormatCtx, packet) < 0) {
            if (is->pFormatCtx->pb->error == 0) {
                SDL_Delay(100); /* no error; wait for user input */
                continue;
            } else {
                break;
            }
        }
        // 将packet存入队列中
        if (packet->stream_index == is->videoStream) {
            packet_queue_put(&is->videoq, packet);
        } else if (packet->stream_index == is->audioStream) {
            packet_queue_put(&is->audioq, packet);
        } else {
            av_packet_unref(packet);
        }
    }
    return 0;
}

视频解码

  1. 从队列中取出视频packet
  2. 解码,同步
  3. 加码后Frame存入数组,等待视频渲染
//// 视频解码
int decode_video_thread(void *arg) {
    VideoState *is = (VideoState *) arg;
    AVPacket pkt1, *packet = &pkt1;
    AVFrame *pFrame;
    double pts;

    pFrame = av_frame_alloc();

    for (;;) {
        // 从视频队列中取出packet
        if (packet_queue_get(&is->videoq, packet, 1) < 0) {
            break;
        }
        
        // 解码
        avcodec_send_packet(is->video_ctx, packet);
        while (avcodec_receive_frame(is->video_ctx, pFrame) == 0) {
            if ((pts = pFrame->best_effort_timestamp) != AV_NOPTS_VALUE) {
            } else {
                pts = 0;
            }
            pts *= av_q2d(is->video_st->time_base);

            // 同步
            pts = synchronize_video(is, pFrame, pts);
            if (queue_picture(is, pFrame, pts) < 0) {
                break;
            }
            av_packet_unref(packet);
        }
    }
    av_frame_free(&pFrame);
    return 0;
}

音频解码

//// 音频设备回调
void audio_callback(void *userdata, Uint8 *stream, int len) {

    VideoState *is = (VideoState *) userdata;
    int len1, audio_size;
    double pts;

    SDL_memset(stream, 0, len);

    while (len > 0) {
        if (is->audio_buf_index >= is->audio_buf_size) {
            // 音频解码
            audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
            if (audio_size < 0) {
                // 音频解码错误,播放静音
                is->audio_buf_size = 1024 * 2 * 2;
                memset(is->audio_buf, 0, is->audio_buf_size);
            } else {
                is->audio_buf_size = audio_size;
            }
            is->audio_buf_index = 0;
        }
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        // 混音播放
        SDL_MixAudio(stream, (uint8_t *) is->audio_buf + is->audio_buf_index, len1, SDL_MIX_MAXVOLUME);
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }
}

视频刷新播放

//// 视频刷新播放,并预测下一帧的播放时间,设置新的定时器
void video_refresh_timer(void *userdata) {

    VideoState *is = (VideoState *) userdata;
    VideoPicture *vp;
    double actual_delay, delay, sync_threshold, ref_clock, diff;

    if (is->video_st) {
        if (is->pictq_size == 0) {
            schedule_refresh(is, 1);
        } else {
            // 从数组中取出一帧视频帧
            vp = &is->pictq[is->pictq_rindex];

            is->video_current_pts = vp->pts;
            is->video_current_pts_time = av_gettime();
            // 当前Frame时间减去上一帧的时间,获取两帧间的时差
            delay = vp->pts - is->frame_last_pts;
            if (delay <= 0 || delay >= 1.0) {
                // 延时小于0或大于1秒(太长)都是错误的,将延时时间设置为上一次的延时时间
                delay = is->frame_last_delay;
            }
            // 保存延时和PTS,等待下次使用
            is->frame_last_delay = delay;
            is->frame_last_pts = vp->pts;

            // 获取音频Audio_Clock
            ref_clock = get_audio_clock(is);
            // 得到当前PTS和Audio_Clock的差值
            diff = vp->pts - ref_clock;

            // AV_SYNC_THRESHOLD 最小刷新时间
            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
            // diff小于非同步阈值,可以进行同步
            if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
                if (diff <= -sync_threshold) {
                    // 视频时间在音频时间之前,应该让视频尽快播放
                    delay = 0;
                } else if (diff >= sync_threshold) {
                     // 视频时间在音频时间之后,应该让视频延迟播放
                    delay = 2 * delay;
                }
            }
            is->frame_timer += delay;
            // 最终真正要延时的时间
            actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
            if (actual_delay < 0.010) {
                // 延时时间过小就设置最小值
                actual_delay = 0.010;
            }
            // 根据延时时间重新设置定时器,刷新视频
            schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));

            // 视频帧显示
            video_display(is);

            // 更新视频帧数组下标
            if (++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
                is->pictq_rindex = 0;
            }
            SDL_LockMutex(is->pictq_mutex);
            // 视频帧数组减一
            is->pictq_size--;
            SDL_CondSignal(is->pictq_cond);
            SDL_UnlockMutex(is->pictq_mutex);
        }
    } else {
        schedule_refresh(is, 100);
    }
}

大体的流程就是这样了,相比之前的Demo复杂度会高不少,但是所有的知识在前面的博客中都有涉及,在博客中也讲不了什么东西,还是直接自己运行,再去看代码会更好,理清流程,整个播放器的代码也不会很难看懂,这里附上源码 Github-SimplePlay

学习音视频推荐:

【腾讯文档】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音视频流媒体高级开发-资料领取
https://docs.qq.com/doc/DYU5ORlBOdkpCUkNxicon-default.png?t=M85Bhttps://docs.qq.com/doc/DYU5ORlBOdkpCUkNx
 

 

使用ffmpeg解码视频渲染到sdl窗口 第一章 视频渲染(本章)第二章 音频(push)播放第三章 音频(pull)播放第四章 实现时钟同步第五章 实现通用时钟同步第六章 实现播放器使用ffmpeg解码视频并渲染视频到窗口,网上是有不少例子的,但是大部分例子的细节都不是很完善,比如资源释放、flush解码缓存、多线程优化等都没有。特别是想要快速搭建一个demo时,总是要重新编写不少代码,比较不方便,所以在这里提供一个完善的例子,可以直接拷贝拿来使用。 阅读详情

相关推荐

FFmpeg3.4+SDL2实现简单视频播放器

先在Ubuntu下安装,测试; FFmpeg播放,需要先安装libx264库,libx264用于解码; 下载路径http://www.videolan.org/developers/x264.html 解压-&gt;配置 ./configure --disable-asm --enbale-shared --enable-static make sudo make install 安装...

cyf15238622067的博客 1057

SDL2 播放音频数据(PCM)

这里以常用的视频原始数据PCM数据为例,展示音频播放。初始化音频子系统:SDL_Init()。设置音频参数:SDL_AudioSpec。设置回调函数:SDL_AudioCallback。打开音频设备:SDL_OpenAudio()。打开pcm文件,读取数据。开始播放SDL_PauseAudio()。int freq;// 音频采样率// 音频数据格式// 声道数: 1 单声道, 2 立体声// 设置静音的值,因为声音采样是有符号的,所以0当然就是这个值。

希望能帮助大家,希望大家多多支持,你们的支持是我前进的动力。 2507

FFmpeg 实战:从零开始写一个简易视频播放器

本文是一篇关于使用 FFmpegSDL2 从零开始编写视频播放器的实战教程。文章详细分解了播放器的核心工作流程,包括解封装、解码、音视频同步和渲染,旨在帮助开发者理解播放器的底层原理,并为进一步实现多线程播放、音画同步等高级功能打下坚实基础

qq_64978731的博客 1511

FFmpeg4.3开发系列之八:SDL2.0小白入门

我将带领大家一起来学习SDL2.0的编程知识,包括SDL2.0的编程框架、原理流程分析,几个核心对象、事件机制、扩展库的使用。具体内容包括:1.搭建开发环境:VS2015或Qt5.9,共两套环境。2.核心对象编程:窗口、表面、渲染器、纹理、事件。3.扩展库的应用:SDL2_image,SDL2_mixer,SDL2_ttf(图像、音频、字库)4.官方案例实战:亲手练习官方推荐的几个案例,认真分析总结。  梅老师从事音视频与流媒体行业18年;曾在永新视博、中科大洋、百度、美国Harris广播事业部等公司就职,经验丰富;曾亲手主导广电直播全套项目,精通h.264/h.265/aac,曾亲自参与百度app上的网页播放器等实战产品。  目前全身心自主创业,主要聚焦音视频+流媒体行业,精通音视频加密、流媒体在线转码快编等热门产品。

使用FFMPEGSDL2实现音视频同步简易视频播放器

这里我简单做了一个视频播放器,并简单做了音视频同步。1.如果当前视频帧与音频帧的播放时间差值小于或等于视频帧持续时间,则表示音视频同步,正常延时。2.如果视频帧比音频帧快,且大于视频帧一帧的时长,延时2倍的正常延时。3.如果视频帧比音频帧慢,且大于视频帧一帧的时长,则立即播放下一帧。4.创建解复用线程,音频解码播放线程,视频解码播放线程。6.根据音频帧和视频帧的差值计算延时。2.将取到的数据送至视频解码器中。5.计算视频帧的pts和持续时间。3.循环从解码器中取解码音频帧。3.循环从解码器中取解码视频帧。

yunxiaobaobei的博客 2474

FFmpeg音视频播放器系列(开篇)

文章目录简介FFMPEG+SDL的视频播放器关键函数与结构体用法说明 简介 学习FFmpeg已经有一段时间了,都是断断续续、零零散散的,没有系统的总结过,为了保证学习效果,决定自己写一个音视频播放器,借着这个项目对FFmpeg进行系统的学习与总结,争取对FFmpeg达到熟练应用的程度。 FFmpeg音视频播放器系列,本着循序渐进的过程,先从最简单的开始,逐步深入到使用QT写出一个带有GUI界面的应...

zhaoyun_zzz的专栏 1万+

ffplay音视频同步与自定义媒体播放器开发实战

FFmpeg是一个非常强大的开源框架,它能够处理几乎所有的音视频格式,包括解码、编码、转码、复用、解复用、流处理等。它支持几乎所有的操作系统,包括Linux、Windows、Mac OS X、BSD等。FFmpeg由Michael Niedermayer在2000年发起,目前已经成为全球最受欢迎的多媒体框架之一。ffplayer是FFmpeg官方提供的简单而强大的命令行多媒体播放器工具。通过本章节,读者将能够掌握ffplayer的基本使用方法,并学会如何基于FFmpeg库创建自定义的多媒体播放器

weixin_42592399的博客 955

基于MFC+FFmpeg+SDL2的视频播放器开发实战项目

FFmpegPlayer是一款基于FFmpeg解码库与SDL2渲染引擎实现的轻量级跨平台音视频播放器。其核心架构采用模块化设计,包含解码、渲染、同步、内存管理及多线程控制等多个关键模块。通过FFmpeg进行音视频流的解析与解码,利用SDL2完成音频输出与视频渲染,项目具备良好的可扩展性与性能表现。为顺利开展后续开发,需搭建基于Windows/Linux的开发环境。

weixin_42430341的博客 683

FFmpegSDL2播放mp4

简单的看了下FFmpegSDL2的相关内容,想用ffmpegSDL2制作一个mp4播放器。网上找了好多例子,大多数只能播放video,不能播放audio,后来还是从一个大神的github里找到一份代码(大神的代码地址),可以实现音视频同步,代码有两个地方需要稍微修改一下,具体的实现思路可以参考大神的博客。现在把代码贴出来,让初学者可以找到一个可以跑起来的代码,增加后续的学习信心。 SimplePlayer.c #include <stdio.h> #include <assert.h&

qq_38694388的博客 884

FFMPEG+SDL2 实现播放器功能

一、界面 1 打开的pushbutton 2 停止/开始的pushbutton 3 进度条 QSlider 4 播放窗口widget 二、创建videothread线程 该线程用于读取视频数据流 具体实现过程可以参考 http://blog.csdn.net/yunge812/article/details/79342089 ...

阳光非宅男的博客 4676

如何开发一个音视频播放器ffmpeg3.2+sdl2.0)

如何开发一个媒体播放器ffmpeg3.2+SDL2.0,视频+音频)前言创建一个媒体播放器的步骤一、播放音频二、播放视频 前言 上一篇讲了如何开发一个音频播放器,其实很简单。总结起来无非就是从文件中获取音频帧,再把音频中的数据传递给系统回调系统直接读取并播放音频的过程。 这次我们更近一步开发一个“视频+音频“”的媒体播放器。同样这次的使用的仍然是ffmpeg3.2+SDL2.0开发的,在阅读时请...

发帖前的格式模板 3811

ffmpeg SDL2.0 播放器GDMPlayer

之前有项目做过双录系统(采集音频、视频原始数据经过H264,ACC编码后合成视频文件)。接触过FFmpegSDL2.0。公司源码是不可能公开的了,于是把之前学习的经历与测试DEMO重新整理了一遍,用VS2010写了个测试工具。本文主要参考ffplay.c下的实现方式。大伙也可以去阅读ffplay.c源码,这里面已经包含很全的视频播放知识了。 : 本播放器基于ffmpeg-4.1.5 与...

780

FFmpeg视频播放器开发实战

FFmpeg是一个开源且功能强大的音视频处理工具集,支持几乎所有的视频和音频格式,被广泛应用于多媒体数据的转码、流媒体服务、音视频播放器等领域。使用FFmpeg构建播放器,意味着你可以快速地为用户提供高质量的媒体播放体验。构建FFmpeg播放器不是一项简单的任务,它涉及到从基本的媒体流解析到音视频同步处理,再到用户界面的交互等多个层面。本章节将带你逐一了解构建过程中的关键步骤,帮助你深入理解并掌握FFmpeg播放器的构建技术。

weixin_29443363的博客 991

Qt5与SDL2打造简易跨平台视频播放器

Qt5是目前广泛使用的跨平台C++应用程序开发框架,由Qt Company开发。它为开发者提供了丰富的API和组件,用于构建具有图形用户界面(GUI)的应用程序。Qt5的模块化设计允许灵活地选择需要的功能组件,从而优化应用程序的大小和性能。框架的信号和槽机制简化了事件驱动编程,使得代码更简洁且易于维护。// 简单的Qt5应用程序示例");在上述代码中,我们创建了一个简单的Qt5应用程序,其中包含一个按钮。

weixin_28872035的博客 1001

FFmpeg简易播放器实现-最简版

1. 视频播放器基本原理 下图引用自“雷霄骅,视音频编解码技术零基础学习方法”,因原图太小,看不太清楚,故重新制作了一张图片。 如下内容引用自“雷霄骅,视音频编解码技术零基础学习方法”: 解协议 将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据...

SurpassLi的专栏 99

【从零手写播放器FFmpeg 音视频开发实战】01-走进音视频的世界

本系列教程将带你从零开始,系统学习音视频开发基础知识,并最终使用 FFmpeg C++ API + SDL2 构建一个功能完备的视频播放器

不吃鱼的猫的博客 354
上一篇: FFmpeg音视频同步
下一篇: FFmpeg从入门到入魔(1):初探FFmpeg框架
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值