基于FFmpeg开发视频播放器,视频解码播放(二)

不念不忘少年蓝@ 2023-07-14 08:13 108阅读 0赞

一,从setDataSource开始,设置播放的数据源,可以时本地视频,也可以是网络链接

  1. EnjoyPlayer.java
  2. private String mPath = "/sdcard/mpeg.mp4";
  3. public void setDataSource(String path) {
  4. setDataSource(nativeHandler, path);
  5. }
  6. EnjoyPlayer.cpp中的 setDataSource,只是简单记录下path,已被prepare使用:
  7. void EnjoyPlayer::setDataSource(const char *path_) {
  8. //C语言的实现,这里地址用自己的成员属性记录,如果直接赋值给path,当path_被释放后,
  9. // path的指向也就无效了,所以这里做一个深拷贝的操作,自己去为指针申请一段内存。
  10. // path = static_cast<char *>(malloc(strlen(path_) + 1));
  11. // memset((void *)path, 0, strlen(path)+1);
  12. // memcpy((void *)path, (void *)path_, strlen(path_));
  13. // C++的实现方式
  14. path = new char[strlen(path_) +1];
  15. strcpy(path, path_);
  16. }
  17. //解析媒体信息,放在一个单独的线程里面执行,比如要解析的是一个来自网络的数据,
  18. void EnjoyPlayer::prepare() {
  19. pthread_create(&prepareTask, 0, prepare_t, this);
  20. }

媒体信息的处理过程:

1,avFormatContext用来保存,打开的媒体文件的构成及上下文信息。

2, 查找媒体流,获取音视频流。注意这里返回值大于等于0表示成功。

3, 针对音频流,视频流,分别获取解码器,虽然ACCodec叫做解码器,但是实际我们解码时并没有直接使用,解码时实际使用的是AVCodecContext,即解码信息上下文,

4,打开解码器

5,通过 AudioChannel,VideoChannel 对音频流,视频流分别处理

6, prepare完成后,通知java层,可以播放了,

  1. void EnjoyPlayer::_prepare() {
  2. //avFormatContext用来保存,打开的媒体文件的构成及上下文信息。
  3. avFormatContext = avformat_alloc_context();
  4. //第一个参数,是一个二级指针,可以修改外部实参,
  5. // 第三个参数表示文件的封装格式avi,flv等,如果传nullptr,会自定识别,
  6. // 第四个参数,表示一个配置信息,比如打开网络文件时,可以指定超时时间
  7. //AVDictionary *options;
  8. //av_dict_set(&options, "timeout", "3000000", 0);
  9. int ret = avformat_open_input(&avFormatContext, path, nullptr, 0);
  10. //打开文件失败,获取失败信息,
  11. if (0 != ret) {
  12. char *msg = av_err2str(ret);
  13. LOGE("打开%s 失败,返回 %d ,错误描述 %s 。", path, ret, msg);
  14. helper->onError(FFMPEG_CAN_NOT_OPEN_URL, THREAD_CHILD);
  15. goto ERROR;
  16. }
  17. //查找媒体流,获取音视频流。注意这里返回值大于等于0表示成功。
  18. ret = avformat_find_stream_info(avFormatContext, 0);
  19. if (ret < 0) {
  20. LOGE("查找媒体流 %s 失败,返回:%d 错误描述:%s", path, ret, av_err2str(ret));
  21. helper->onError(FFMPEG_CAN_NOT_FIND_STREAMS, THREAD_CHILD);
  22. goto ERROR;
  23. }
  24. //获取视频时长,默认返回值单位为微妙,这里转成秒数。
  25. duration = avFormatContext->duration/AV_TIME_BASE;
  26. //获取媒体文件中的媒体流(音频流,视频流)
  27. for (int i = 0; i < avFormatContext->nb_streams; ++i) {
  28. AVStream *avStream = avFormatContext->streams[i];
  29. //获取媒体流上的解码信息,配置信息,参数信息
  30. AVCodecParameters *parameters = avStream->codecpar;
  31. //查找相关的解码器,但是这个函数可能返回null,就是没有找到流对应的解码器,
  32. // 因为在编译ffmpeg时,配置信息指定了是否开启特定格式的编解码支持,
  33. // 可以通过命令:ffmpeg -decoders查看支持的解码格式。
  34. AVCodec *dec = avcodec_find_decoder(parameters->codec_id);
  35. if (!dec) {
  36. helper->onError(FFMPEG_FIND_DECODER_FAIL, THREAD_CHILD);
  37. goto ERROR;
  38. }
  39. //尽管ACCodec叫做解码器,但是实际我们解码时并没有直接使用,解码时实际使用的是AVCodecContext,即解码信息上下文,
  40. //这里实际就是为结构体申请内存。
  41. AVCodecContext *avCodecContext = avcodec_alloc_context3(dec);
  42. //把解码信息,赋值给解码上下文中的成员变量
  43. if (avcodec_parameters_to_context(avCodecContext, parameters) < 0) {
  44. helper->onError(FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL, THREAD_CHILD);
  45. goto ERROR;
  46. }
  47. //打开解码器
  48. if (avcodec_open2(avCodecContext, dec, 0) != 0) {
  49. helper->onError(FFMPEG_OPEN_DECODER_FAIL, THREAD_CHILD);
  50. goto ERROR;
  51. }
  52. //对音频流,视频流分别处理
  53. if (parameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  54. audioChannel = new AudioChannel(i, helper, avCodecContext, avStream->time_base);
  55. } else if (parameters->codec_type == AVMEDIA_TYPE_VIDEO){
  56. //从流身上,拿到视频的帧率,就是通过avg_frame_rate的分子除以分母
  57. int fps = av_q2d(avStream->avg_frame_rate);
  58. if (isnan(fps) || fps == 0) {
  59. //如果获取平均帧率失败,尝试去获取基础帧率
  60. fps = av_q2d(avStream->r_frame_rate);
  61. }
  62. if (isnan(fps) || fps == 0) {
  63. //上面两种情况都获取不到帧率,将根据container,codec猜测一个值,
  64. // 因为fps会在音视频同步时用到,所以一定要有值。
  65. fps = av_q2d(av_guess_frame_rate(avFormatContext, avStream, 0));
  66. }
  67. videoChannel = new VideoChannel(i, helper, avCodecContext, avStream->time_base, fps);
  68. videoChannel->setWindow(window);
  69. }
  70. }
  71. //判断媒体文件是否包含,音频,视频,
  72. if (!videoChannel && !audioChannel) {
  73. helper->onError(FFMPEG_NOMEDIA,THREAD_CHILD);
  74. goto ERROR;
  75. }
  76. //通知java层,可以播放了,
  77. helper->onPrepare(THREAD_CHILD);
  78. return;
  79. ERROR:
  80. LOGE("解析媒体文件失败。。。");
  81. _release();
  82. }

prepare之后,就可以start播放了,在这之前,还要一个重要的参数需要设置,就是Surface,为的是从这个Surface中拿到ANativeWindow,在Native层去渲染图像,通常要借助ANativeWindow,其实Java层的View组件,通过Canvas去draw内容,底层也是借助的AnativeWidnow完成的绘制.

  1. MainActivity.java
  2. public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
  3. Surface surface = holder.getSurface();
  4. mEnjoyPlayer.setSurface(surface);
  5. }

通过ANativeWindow *window = ANativeWindow_fromSurface(env, surface);从surface拿到关联的ANativeWindow.

  1. extern "C"
  2. JNIEXPORT void JNICALL
  3. Java_com_test_ffmpegapplication_EnjoyPlayer_setSurface(JNIEnv *env, jobject thiz,
  4. jlong native_handler, jobject surface) {
  5. EnjoyPlayer *enjoyPlayer = reinterpret_cast<EnjoyPlayer *>(native_handler);
  6. ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
  7. enjoyPlayer->setWindow(window);
  8. }
  9. 启动播放:读取媒体源的数据,需要单独一个线程来操作读取,
  10. //根据类型放入Audio vidoe channel队列中,
  11. EnjoyPlayer.cpp
  12. void EnjoyPlayer::start() {
  13. //读取媒体源的数据,需要单独一个线程来操作读取,并且线程可以停止,
  14. //根据类型放入Audio vidoe channel队列中,
  15. isPlaying = 1;
  16. if (videoChannel) {
  17. videoChannel->audioChannel = audioChannel;
  18. videoChannel->play();
  19. }
  20. if (audioChannel){
  21. audioChannel->play();
  22. }
  23. pthread_create(&startTask, 0, start_t, this);
  24. }

这里只关注视频的处理,要完成播放,就要先解码,然后把解码后的数据转成本地窗口需要格式,填充到Window中.

这里开启两个线程,一个负责解码,一个负责绘制

  1. void VideoChannel::play() {
  2. isPlaying = 1;
  3. setEnable(true);
  4. //做两个事情,解码,播放,分别在单独的线程执行,会比较流畅
  5. pthread_create(&videoDecodeTask, 0 ,videoDecode_t, this);
  6. pthread_create(&videoPlayTask, 0, videoPlay_t, this);
  7. }

解码的过程,就是从packet_queue队列中取出AVPacket,交给avcodec去解码,然后拿到解码后的AVFrame数据,放入frame_queue队列,供播放线程使用.

  1. void VideoChannel::decode() {
  2. AVPacket *packet=0;
  3. while (isPlaying) {
  4. //dequeu是一个阻塞操作,取出待解码数据
  5. int ret = pkt_queue.deQueue(packet);
  6. //向解码器发送解码数据
  7. ret = avcodec_send_packet(avCodecContext, packet);
  8. //因为packet存放于堆内存,所以用完释放。
  9. releaseAvPacket(packet);
  10. if (ret < 0) {
  11. break;
  12. }
  13. //从解码器取出解码好的数据
  14. AVFrame *avFrame = av_frame_alloc();
  15. ret = avcodec_receive_frame(avCodecContext, avFrame);
  16. if (ret == AVERROR(EAGAIN)) {
  17. //表示需要更多的待解码数据,
  18. continue;
  19. } else if (ret < 0) {
  20. break;
  21. }
  22. frame_queue.enQueue(avFrame);
  23. }
  24. releaseAvPacket(packet);
  25. }

绘制的过程:

1,YUV转RGB 通常用SwsContext来做格式转换

2,把转化后的数据,显示到window上,

3,设置window属性,获取window上数据的缓冲区,把视频数据刷到buffer中。

  1. void VideoChannel::_play() {
  2. AVFrame *frame = 0;
  3. int ret;
  4. uint8_t *data[4];
  5. int linesize[4];
  6. //通常用SwsContext来做格式转换,YUV转RGB,缩放,
  7. // 或者加滤镜效果等,也可以在这个阶段做,
  8. SwsContext *swsContext = sws_getContext(avCodecContext->width,
  9. avCodecContext->height,
  10. avCodecContext->pix_fmt,
  11. avCodecContext->width,
  12. avCodecContext->height,
  13. AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR,
  14. 0,0,0);
  15. //根据传入的参数,确定申请的内存大小,
  16. av_image_alloc(data, linesize, avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);
  17. double frame_delay = 1.0 / fps;
  18. while (isPlaying) {
  19. ret = frame_queue.deQueue(frame);
  20. if (!isPlaying) {//如果停止播放,退出处理
  21. break;
  22. }
  23. if (!ret) {
  24. continue;
  25. }
  26. //根据帧率,再参考额外延迟时间repeat_pict,让视频播放更流畅。delay是要让视频以正常的速度播放,
  27. double extra_delay = frame->repeat_pict / (2*fps);
  28. double delay = extra_delay + frame_delay;
  29. if (audioChannel) {
  30. //处理音视频同步,best_effort_timestamp跟pts通常是一致的,
  31. // 区别是best_effort_timestamp经过了一些参考,得到一个最优的时间
  32. clock = frame->best_effort_timestamp * av_q2d(time_base); //视频的时钟,
  33. double diff = clock - audioChannel->clock;
  34. //音频,视频的时间戳 的差,这个差有一个允许的范围(0.04 ~ 0.1)
  35. double sync = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
  36. if (diff <= -sync) {
  37. delay = FFMAX(0, delay + diff); //视频慢了
  38. } else if (diff > sync) {
  39. delay = delay + diff;
  40. }
  41. LOGE("clock ,video:%1f ,audio:%1f, delay:%1f, V-A = %1f ",clock, audioChannel->clock, delay, diff);
  42. }
  43. av_usleep(delay * 1000000);
  44. //第二个参数代表图像数据,是一个二维数组(每一维度,代表RGBA中的一个数据),所以是一个指向指针的指针,
  45. // 每一个维度的数据就是一个指针,那么RGBA需要4个指针,所以就是4个元素的数组,数组的元素就是指针,指针数据
  46. //第三个参数,每一行数据,的 数据个数
  47. //第四个,从原始图像的那个位置开始转换,
  48. //第五个,图像的高度,
  49. //最后两个参数,得到转换后的数据结果,所以变量类型跟第二个,第三个参数一样,
  50. sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height, data, linesize);
  51. //把转化后的数据,显示到window上,
  52. onDraw(data, linesize, avCodecContext->width, avCodecContext->height);
  53. releaseAvFrame(frame);
  54. }
  55. av_free(&data[0]);
  56. isPlaying = 0;
  57. releaseAvFrame(frame);
  58. sws_freeContext(swsContext);
  59. }

视频数据绘制, 涉及对window的操作,都要加锁,

  1. void VideoChannel::onDraw(uint8_t **data, int *linesize, int width, int height) {
  2. pthread_mutex_lock(&surfaceMutex);
  3. if (!window) {
  4. //window还不可用
  5. pthread_mutex_unlock(&surfaceMutex);
  6. return;
  7. }
  8. //把window的宽高(是指显示像素的宽高,不是物理宽高,也不是view的宽高)设置为跟要显示图像的宽高一样,
  9. // 这样可以保证原始视频的宽高比
  10. ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
  11. //window上数据的缓冲区,
  12. ANativeWindow_Buffer buffer;
  13. if (ANativeWindow_lock(window, &buffer, 0) !=0) {
  14. ANativeWindow_release(window);
  15. window =0;
  16. pthread_mutex_unlock(&surfaceMutex);
  17. return;
  18. }
  19. //把视频数据刷到buffer中。
  20. //得到RGBA格式的数据后,开始想ANativeWindow填充,但是在数据填充时,
  21. // 需要根据window——buffer的步进来一行一行的拷贝,window_buffer.stride。
  22. //因为涉及字节对齐,window的一行数据数,跟图像数据的一行数据数不同,所以要一行一行拷贝。
  23. // 所谓字节对齐,比如说以12字节对齐为例,当一个数据大小不足12字节,比如只有10字节,,
  24. // 会添加2个占位字节,补齐12字节,
  25. //窗口buffer中需要的数据。
  26. uint8_t *dstData = static_cast<uint8_t *>(buffer.bits);
  27. int dstSize = buffer.stride * 4;//乘4,RGBA_8888占4个字节,
  28. //data原本是一个指针的指针(4个维度),在经过sws_scale转换后,所有的RGBA数据都保存在第一个指针中了。
  29. //视频图像的RGBA数据,
  30. uint8_t *srcData = data[0];
  31. int srcSize = linesize[0];
  32. //一行一行的拷贝
  33. for (int i = 0; i < buffer.height; ++i) {
  34. memcpy(dstData+i*dstSize, srcData+ i*srcSize, srcSize);
  35. }
  36. ANativeWindow_unlockAndPost(window);
  37. pthread_mutex_unlock(&surfaceMutex);
  38. }

到这里,就完成了视频数据的绘制.

附件(源码):https://download.csdn.net/download/lin20044140410/12247488

发表评论

表情:
评论列表 (有 0 条评论,108人围观)

还没有评论,来说两句吧...

相关阅读