AndroidNDK FFmpeg解码播放mp4文件
2022-05-05 本文已影响0人
Analyas
在使用FFmpeg解码播放视频时,遇到了不少坑,如果你参照官方decode_video.c的demo非常容易越陷越深,一不小心就走错方向了,比如av_parser_parse2的作用是将编码后的裸流数据(如H264, H265等)数据合并成一个packet,再由packet如解析出一个AVFrame帧数据,你不能使用av_parser_parse2去解析一个mp4媒体文件,因为他不仅包含视频流,还包含着其它如音频之类的流媒体信息,我也是查了好久才发现以下文章有提到过的,大家可以去了解一下
https://blog.actorsfit.com/a?ID=01050-8e62efbd-a0c3-4e98-aca2-8cc648be9be7
话不多说,直接上代码,已经简化不必要的代码
1.activity_mp4_decode.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_display_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#000"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
2.Mp4DecodeActivity.java
package com.qmel.surfacework;
import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
public class Mp4DecodeActivity extends AppCompatActivity {
//JNI解码接口
static {
System.loadLibrary("avformat");
System.loadLibrary("avcodec");
System.loadLibrary("avutil");
System.loadLibrary("swscale");
System.loadLibrary("swresample");
System.loadLibrary("avfilter");
//decodeMp4 JNI实现方法所在的so库
System.loadLibrary("surfacework");
}
public native void decodeMp4(String filePath, CallBack callBack);
private ImageView displayImageView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mp4_decode);
displayImageView = findViewById(R.id.iv_display_image);
//检查文件访问权限,如果给了权限,需要退出重新打开应用
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
Toast.makeText(getApplicationContext(), "请授权存储空间访问权限后再次打开应用!", Toast.LENGTH_LONG).show();
return;
}
//开启线程去解码文件,Demo目前只解码视频,音频的需要自己处理了,PTS没做处理,现在是加速播放,正常播放请自己手动去处理PTS之类的东西
new Thread(){
@Override
public void run() {
//要解码的视频文件路径、解码回调接口
decodeMp4("/storage/emulated/0/Pictures/aging_video_high.mp4", callBack);
}
}.start();
}
//解码视频后的回调接口
public interface CallBack {
void onGetDecodeData(int[] pixels, int width, int height);
}
private final CallBack callBack = new CallBack() {
Bitmap lastBitmap;
@Override
public void onGetDecodeData(int[] pixels, int width, int height) {
//得到RGB像素数据后生成Bitmap,并回收之前的图片,避免内存泄漏
Bitmap bitmap = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
runOnUiThread(()->{
displayImageView.setImageBitmap(bitmap);
if (lastBitmap != null && !lastBitmap.isRecycled()) {
lastBitmap.recycle();
}
lastBitmap = bitmap;
});
}
};
}
3.decode_mp4.cpp
//
// Created by Administrator on 5/5/2022.
//
#include <jni.h>
#include <android/log.h>
extern "C" {
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavfilter/avfilter.h"
#include "libavutil/imgutils.h"
JNIEXPORT void JNICALL
Java_com_qmel_surfacework_Mp4DecodeActivity_decodeMp4(JNIEnv *env, jobject thiz, jstring filePath,
jobject callback) {
//1.打开视频文件,获取视频信息,如果失败,检查程序是否有访问权限(必要)
AVFormatContext *avFormatContext = NULL;
const char *filepath = env->GetStringUTFChars(filePath, nullptr);
int open_status = avformat_open_input(&avFormatContext, filepath, nullptr, nullptr);
if (open_status != 0) {
char message[256];
sprintf(message, "无法打开视频文件,错误代码:%d:%s", open_status, av_err2str(open_status));
env->ThrowNew(env->FindClass("java/lang/Exception"), message);
return;
}
//获取视频轨道在stream中的index
int videoIndex = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
//根据视频信息,查找指定解码器,请注意,如果AVCodec指针为0x0,表示没有这样的解码器,mp4一般是h264
const AVCodec *avCodec = avcodec_find_decoder(avFormatContext->streams[videoIndex]->codecpar->codec_id);
__android_log_print(ANDROID_LOG_ERROR, "FFMpeg调试信息", "AVCodec指针:%p", avCodec);
//拿到视频的宽高信息
jint width = avFormatContext->streams[videoIndex]->codecpar->width;
jint height = avFormatContext->streams[videoIndex]->codecpar->height;
//2.创建解码器,并初始化解码器上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
//解码线程数
avCodecContext->thread_count = 16;
//将视频流的基本信息传递到解码器上下文中(看codec_par.c源码其实主要传递了PIX_FMT,宽高,色彩信息以及视频的extradata信息)
avcodec_parameters_to_context(avCodecContext, avFormatContext->streams[videoIndex]->codecpar);
//打开解码器,注意:在open前请先调用avcodec_parameters_to_context把视频参数附加到上下文,不然可能会解码出错
int codecIsOpen = avcodec_open2(avCodecContext, avCodec, nullptr);
if (codecIsOpen < 0) {
char message[256];
sprintf(message, "解码器打开失败: %d==%d", codecIsOpen, avCodec->id);
env->ThrowNew(env->FindClass("java/lang/Exception"), message);
return;
}
//3.初始化解码的一些配置(通常来说必要)
//创建swsContext,用于转换解码出来的视频帧格式,因为解析mp4出来的是yuv420p格式,但我需要RGB888格式(这里看自己需要)
SwsContext *swsContext = sws_getContext(width, height, AV_PIX_FMT_YUV420P, width, height,
AV_PIX_FMT_RGB24, SWS_BILINEAR,
NULL, NULL, NULL);
//解码后的YUV帧
AVFrame *avDecodeFrame = av_frame_alloc();
//转成后的RGB帧
AVFrame *rgbFrame = av_frame_alloc();
//编码的packet数据缓存
AVPacket *avDecodePacket = av_packet_alloc();
//分配RGB帧的缓存大小,有于YUV在解码时会自动帮我们设置,但使用sws转换的不会帮我们去设置,所以这里是必要的
int allocImageSize = av_image_alloc(rgbFrame->data, rgbFrame->linesize, width, height, AV_PIX_FMT_RGB24, 1);
//分配空间对于RGB来说一般返回结果是width*height*3,如果小于等于0,那么失败,最后一个参数align一般是1,测试0是分配失败的
if (allocImageSize <= 0){
char message[256];
sprintf(message, "无法分配RGB缓存空间,av_image_alloc返回值:%d", allocImageSize);
env->ThrowNew(env->FindClass("java/lang/Exception"), message);
return;
}
//4.初始化Java层的回调
jclass javaClazz = env->FindClass("com/qmel/surfacework/Mp4DecodeActivity$CallBack");
jmethodID methodId = env->GetMethodID(javaClazz, "onGetDecodeData", "([III)V");
//存储rgb pixel像素的java数组
jintArray pixelArray = env->NewIntArray(width * height);
jint pixelCount = width * height;
jint *toPixels = (jint *) malloc(pixelCount * sizeof(jint));
//5.开始解码
while (true) {
//读取一帧Packet, 注意:该Packet可能是视频帧也可能是音频帧,注意根据stream_index做判断
int readFrameStatus = av_read_frame(avFormatContext, avDecodePacket);
if (readFrameStatus == AVERROR_EOF) {
__android_log_print(ANDROID_LOG_ERROR, "FFMpeg调试信息", "读取完毕");
break;
} else if (avDecodePacket->stream_index != videoIndex){
continue;
}
//发送编码的Packet到解码器
int sendPacketStatus = avcodec_send_packet(avCodecContext, avDecodePacket);
if (sendPacketStatus < 0 || sendPacketStatus == AVERROR(EAGAIN)) {
char message[256];
sprintf(message, "发送数据到解码器失败,错误代码:%s", av_err2str(sendPacketStatus));
__android_log_print(ANDROID_LOG_ERROR, "FFMpeg调试信息", "%s", message);
continue;
}
//接收解码后的数据
int receiveFrameStatus = avcodec_receive_frame(avCodecContext, avDecodeFrame);
if (receiveFrameStatus == AVERROR(EAGAIN)) {
continue;
} else if (receiveFrameStatus < 0) {
char message[256];
sprintf(message, "获取解码数据失败,错误信息:%s", av_err2str(receiveFrameStatus));
env->ThrowNew(env->FindClass("java/lang/Exception"), message);
return;
}
__android_log_print(ANDROID_LOG_ERROR, "FFmpeg解码", "解码视频帧宽高:%d x %d, linesize:%d, Format:%d",
avDecodeFrame->width,
avDecodeFrame->height,
avDecodeFrame->linesize[0],
avDecodeFrame->format);
//将YUV视频帧转换成RGB视频帧
sws_scale(swsContext, avDecodeFrame->data, avDecodeFrame->linesize, 0, height
, rgbFrame->data, rgbFrame->linesize);
///////////////////////将数据转换成Bitmap需要的int[] RGB数组/////////////////////////////
//源数据
uint8_t *rgbBytes = rgbFrame->data[0];
//开始进行像素的运算
int startIndex = 0;
for (int he = 0; he < height; ++he) {
for (int wi = 0; wi < width; ++wi) {
//由于opengl的第一个像素是从左下角开始数起,所以第一个像素应该从左上角开始设置
jint pixel = ((int32_t) 0xff000000) | ((rgbBytes[startIndex] & 0xff) << 16)
| ((rgbBytes[startIndex + 1] & 0xff) << 8)
| ((rgbBytes[startIndex + 2] & 0xff));
toPixels[(he * width + wi)] = pixel;
startIndex = startIndex + 3;
}
}
//将像素数据放入java层的数组中
env->SetIntArrayRegion(pixelArray, 0, pixelCount, toPixels);
//释放内存
if (callback != nullptr) {
env->CallVoidMethod(callback, methodId, pixelArray, width, height);
}
}
//释放资源
free(toPixels);
av_packet_free(&avDecodePacket);
av_frame_free(&avDecodeFrame);
av_frame_free(&rgbFrame);
avcodec_close(avCodecContext);
avformat_close_input(&avFormatContext);
sws_freeContext(swsContext);
env->DeleteLocalRef(javaClazz);
env->DeleteLocalRef(pixelArray);
}
}
关于FFmpeg源码的导入这里不过多解释,大家有兴趣的话可以看我之前的文章
(1) Android NDK编译和导入FFmpeg源码
欢迎加入Android开发学习群:202253703