作者:字节流动
来源:https://blog.csdn.net/Kennethdroid/article/details/86418725
封装格式
我们经常所说的视频格式,如 mp4 、 mkv 、 rmvb 、flv 等,表示的是音视频的封装格式,封装格式实质上是把音频数据、视频数据和字幕数据打包成一个文件的规范。从技术的角度来讲,优秀的音视频封装格式应该支持大多数音视频编码标准。
主要的封装格式:
名称 | 机构 | 支持的视频编码 | 支持的音频编码 | 使用领域 |
AVI | 微软 | 几乎所有格式 | 几乎所有格式 | BT 下载影视 |
MP4 | MPEG | MPEG-4 , H.264 , H.263 等 | AAC , MPEG-1 等 | 互联网视频网站 |
FLV | Adobe | VP6 , H.264 | MP3 , AAC 等 | 互联网视频网站 |
MKV | CoreCodec | 几乎所有格式 | 几乎所有格式 | 互联网视频网站 |
RMVB | Real Networks | RealVideo 8 , 9 , 10 | AAC , Cook Codec | BT 下载影视 |
编码格式
编码的目的在于通过压缩算法降低数据量,提高数据的存储和传输效率。视频编码是将视频像素数据( RGB , YUV 等)压缩成为视频码流。音频编码是将音频采样数据( PCM 等)压缩成为音频码流。
主要视频编码格式:
名称 | 机构 | 推出时间 | 使用领域 |
H.265 | MPEG/ITU-T | 2013 | 研发中 |
H.264 | MPEG/ITU-T | 2003 | 各个领域 |
MPEG4 | MPEG | 2001 | 小众 |
MPEG2 | MPEG | 1994 | 数字电视 |
VP9 | 2013 | 研发中 | |
VP8 | 2008 | 小众 |
主要音频编码格式:
名称 | 机构 | 推出时间 | 使用领域 |
AAC | MPEG | 1997 | 各个领域 |
AC-3 | Dolby | 1992 | 电影 |
MP3 | MPEG | 1993 | 早期普及 |
WMV | 微软 | 1999 | Windows |
音视频解码流程
- 解封装格式。将输入的按照一定格式封装的音视频数据,分离成为音频流压缩编码数据和视频流压缩编码数据。
- 解码。将视频和音频的压缩编码数据,解码成为非压缩的视频和音频原始数据。视频压缩数据通过解码输出为像素数据,如 YUV420P 、 RGB 等;音频压缩数据通过解码输出为非压缩的音频抽样数据,如 PCM 数据。
- 音视频同步。同步解码出来的视频和音频数据,并将音视频数据送至系统的声卡和显卡,播放和显示出来。
FFmpeg 函数库
FFmpeg 一般有 8 个函数库,各个函数库的功能如下:
函数库 | 功能 |
avcodec | 音视频编解码 |
avdevice | 多媒体设备输入输出 |
avfilter | 滤镜特效 |
avformat | 封装格式处理 |
postproc | 后加工 |
avutil | 工具库 |
swresample | 音频采样数据格式转换 |
swscale | 视频像素数据格式转换 |
FFmpeg 音视频解码
FFmpeg 音视频解码主要流程代码描述:
1. av_register_all() //注册组件 2. avformat_alloc_context //获取封装格式上下文 3. avformat_find_stream_info //获取输入文件信息 4. avcodec_find_decoder //获取解码器 5. avcodec_open2 //打开解码器 6. avcodec_decode_video2 或 avcodec_decode_audio4 //解码音视频帧
在 AS 工程中引入 FFmpeg 8 个动态库和 libyuv (负责视频像素数据格式转换)动态库。
工程的头文件目录:
工程的动态库目录:
Java 层 API :
package com.haohao.ffmpeg; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.util.Log; import android.view.Surface; /** * author: haohao * time: 2017/12/19 * mail: haohaochang86@gmail.com * desc: AVUtils */ public class AVUtils { private static final String TAG = "AVUtils"; private static AVCallback AVCallback; private static AVCallback sAVCallback; public static void registerCallback(AVCallback callback) { sAVCallback = callback; } static { System.loadLibrary("avfilter-5"); System.loadLibrary("avdevice-56"); System.loadLibrary("yuv"); System.loadLibrary("avutil-54"); System.loadLibrary("swresample-1"); System.loadLibrary("avcodec-56"); System.loadLibrary("avformat-56"); System.loadLibrary("swscale-3"); System.loadLibrary("postproc-53"); System.loadLibrary("native-lib"); } /** * 解码视频中的视频压缩数据 * @param input_file_path 输入的视频文件路径 * @param output_file_path 视频压缩数据解码后输出的 YUV 文件路径 */ public static native void videoDecode(String input_file_path, String output_file_path); /** * 显示视频视频解码后像素数据 * @param input 输入的视频文件路径 * @param surface 用于显示视频视频解码后的 RGBA 像素数据 */ public static native void videoRender(String input, Surface surface); /** * 解码视频中的音频压缩数据 * @param input 输入的视频文件路径 * @param output 音频压缩数据解码后输出的 PCM 文件路径 */ public static native void audioDecode(String input, String output); /** * 播放视频中的音频数据 * @param input 输入的视频文件路径 */ public static native void audioPlay(String input); /** * 创建一个 AudioTrack 对象,用于播放音频,在 Native 层中调用。 */ public static AudioTrack createAudioTrack(int sampleRate, int num_channel) { int audioFormat = AudioFormat.ENCODING_PCM_16BIT; Log.i(TAG, "声道数:" + num_channel); int channelConfig; if (num_channel == 1) { channelConfig = android.media.AudioFormat.CHANNEL_OUT_MONO; } else if (num_channel == 2) { channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO; } else { channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO; } int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioTrack audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, bufferSize, AudioTrack.MODE_STREAM); return audioTrack; } public interface AVCallback { void onFinish(); } }
MySurfaceView.java
/** * author: haohao * time: 2017/12/20 * mail: haohaochang86@gmail.com * desc: MySurfaceView */ public class MySurfaceView extends SurfaceView { public MySurfaceView(Context context) { super(context); } public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); } public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private void init(){ // 设置像素绘制格式为 RGBA_8888 SurfaceHolder holder = getHolder(); holder.setFormat(PixelFormat.RGBA_8888); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.haohao.ffmpeg.MySurfaceView android:id="@+id/my_surface_view" android:layout_width="match_parent" android:layout_height="match_parent" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:alpha="0.7" android:orientation="horizontal"> <Button android:id="@+id/video_decode_btn" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="视频解码" /> <Button android:id="@+id/video_render_btn" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="视频渲染" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:alpha="0.7" android:orientation="horizontal"> <Button android:id="@+id/audio_decode_btn" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="音频解码" /> <Button android:id="@+id/audio_play_btn" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="音频播放" /> </LinearLayout> </LinearLayout> </FrameLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener, AVUtils.AVCallback { private static final String TAG = "MainActivity"; private static final String BASE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar; private String input_video_file_path = BASE_PATH + "input.mp4"; private String output_video_file_path = BASE_PATH + "output.yuv"; private String input_audio_file_path = BASE_PATH + "hello.mp3"; private String output_audio_file_path = BASE_PATH + "hello.pcm"; private String video_src = BASE_PATH + "ffmpeg.mp4"; private Button mDecodeVideoBtn; private Button mVideoRenderBtn; private Button mAudioPlayBtn, mAudioDecodeBtn; private ProgressDialog mProgressDialog; private ExecutorService mExecutorService; private MySurfaceView mySurfaceView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS}, 0); } mDecodeVideoBtn = (Button)findViewById(R.id.video_decode_btn); mVideoRenderBtn = (Button)findViewById(R.id.video_render_btn); mAudioDecodeBtn = (Button) findViewById(R.id.audio_decode_btn); mAudioPlayBtn = (Button)findViewById(R.id.audio_play_btn); mySurfaceView = (MySurfaceView) findViewById(R.id.my_surface_view); mDecodeVideoBtn.setOnClickListener(this); mVideoRenderBtn.setOnClickListener(this); mAudioDecodeBtn.setOnClickListener(this); mAudioPlayBtn.setOnClickListener(this); AVUtils.registerCallback(this); mProgressDialog = new ProgressDialog(this); mProgressDialog.setCanceledOnTouchOutside(false); mExecutorService = Executors.newFixedThreadPool(2); } @Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.video_decode_btn: mProgressDialog.setMessage("正在解码..."); mProgressDialog.show(); mExecutorService.submit(new Runnable() { @Override public void run() { AVUtils.videoDecode(input_video_file_path, output_video_file_path); } }); break; case R.id.video_render_btn: mExecutorService.submit(new Runnable() { @Override public void run() { AVUtils.videoRender(input_video_file_path, mySurfaceView.getHolder().getSurface()); } }); break; case R.id.audio_decode_btn: mProgressDialog.setMessage("正在解码..."); mProgressDialog.show(); mExecutorService.submit(new Runnable() { @Override public void run() { AVUtils.audioDecode(input_audio_file_path, output_audio_file_path); } }); break; case R.id.audio_play_btn: mExecutorService.submit(new Runnable() { @Override public void run() { AVUtils.audioPlay(input_video_file_path); } }); break; } } @Override public void onFinish() { runOnUiThread(new Runnable() { @Override public void run() { if (mProgressDialog.isShowing()) { mProgressDialog.dismiss(); } Toast.makeText(MainActivity.this, "解码完成", Toast.LENGTH_SHORT).show(); } }); } @Override protected void onDestroy() { super.onDestroy(); mExecutorService.shutdown(); } }
nativelib.c
#include <jni.h> #include <string.h> #include <android/log.h> #include <stdio.h> #include <libavutil/time.h> //编码 #include "include/libavcodec/avcodec.h" //封装格式处理 #include "include/libavformat/avformat.h" //像素处理 #include "include/libswscale/swscale.h" #define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"haohao",FORMAT,##__VA_ARGS__); #define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"haohao",FORMAT,##__VA_ARGS__); //中文字符串转换 jstring charsToUTF8String(JNIEnv *env, char *s) { jclass string_cls = (*env)->FindClass(env, "java/lang/String"); jmethodID mid = (*env)->GetMethodID(env, string_cls, "<init>", "([BLjava/lang/String;)V"); jbyteArray jb_arr = (*env)->NewByteArray(env, strlen(s)); (*env)->SetByteArrayRegion(env, jb_arr, 0, strlen(s), s); jstring charset = (*env)->NewStringUTF(env, "UTF-8"); return (*env)->NewObject(env, string_cls, mid, jb_arr, charset); } JNIEXPORT void JNICALL Java_com_haohao_ffmpeg_AVUtils_videoDecode(JNIEnv *env, jclass type, jstring input_, jstring output_) { //访问静态方法 jmethodID mid = (*env)->GetStaticMethodID(env, type, "onNativeCallback", "()V"); //需要转码的视频文件(输入的视频文件) const char *input = (*env)->GetStringUTFChars(env, input_, 0); const char *output = (*env)->GetStringUTFChars(env, output_, 0); //注册所有组件 av_register_all(); //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息 AVFormatContext *pFormatCtx = avformat_alloc_context(); //打开输入视频文件 if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) { LOGE("%s", "无法打开输入视频文件"); return; } //获取视频文件信息,例如得到视频的宽高 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { LOGE("%s", "无法获取视频文件信息"); return; } //获取视频流的索引位置 //遍历所有类型的流(音频流、视频流、字幕流),找到视频流 int v_stream_idx = -1; int i = 0; for (; i < pFormatCtx->nb_streams; i++) { //判断视频流 if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } } if (v_stream_idx == -1) { LOGE("%s", "找不到视频流\n"); return; } //根据视频的编码方式,获取对应的解码器 AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec; //根据编解码上下文中的编码 id 查找对应的解码器 AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { LOGE("%s", "找不到解码器,或者视频已加密\n"); return; } //打开解码器,解码器有问题(比如说我们编译FFmpeg的时候没有编译对应类型的解码器) if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { LOGE("%s", "解码器无法打开\n"); return; } //输出视频信息 LOGI("视频的文件格式:%s", pFormatCtx->iformat->name); LOGI("视频时长:%lld", (pFormatCtx->duration) / (1000 * 1000)); LOGI("视频的宽高:%d,%d", pCodecCtx->width, pCodecCtx->height); LOGI("解码器的名称:%s", pCodec->name); //准备读取 //AVPacket用于存储一帧一帧的压缩数据(H264) //缓冲区,开辟空间 AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket)); //AVFrame用于存储解码后的像素数据(YUV) //内存分配 AVFrame *pFrame = av_frame_alloc(); //YUV420 AVFrame *pFrameYUV = av_frame_alloc(); //只有指定了AVFrame的像素格式、画面大小才能真正分配内存 //缓冲区分配内存 uint8_t *out_buffer = (uint8_t *) av_malloc( avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); //初始化缓冲区 avpicture_fill((AVPicture *) pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等 struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); int got_picture, ret; //输出文件 FILE *fp_yuv = fopen(output, "wb+"); int frame_count = 0; //一帧一帧的读取压缩数据 while (av_read_frame(pFormatCtx, packet) >= 0) { //只要视频压缩数据(根据流的索引位置判断) if (packet->stream_index == v_stream_idx) { //解码一帧视频压缩数据,得到视频像素数据 ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); if (ret < 0) { LOGE("%s", "解码错误"); return; } //为 0 说明解码完成,非0正在解码 if (got_picture) { //AVFrame转为像素格式YUV420,宽高 //2 6输入、输出数据 //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的 //4 输入数据第一列要转码的位置 从0开始 //5 输入画面的高度 sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); //输出到YUV文件 //AVFrame像素帧写入文件 //data解码后的图像像素数据(音频采样数据) //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感 //U V 个数是Y的1/4 int y_size = pCodecCtx->width * pCodecCtx->height; fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv); fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv); fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv); frame_count++; LOGI("解码第%d帧", frame_count); } } //释放资源 av_free_packet(packet); } fclose(fp_yuv); av_frame_free(&pFrame); avcodec_close(pCodecCtx); avformat_free_context(pFormatCtx); (*env)->ReleaseStringUTFChars(env, input_, input); (*env)->ReleaseStringUTFChars(env, output_, output); //通知 Java 层解码完毕 (*env)->CallStaticVoidMethod(env, type, mid); } //使用这两个 Window 相关的头文件需要在 CMake 脚本中引入 android 库 #include <android/native_window_jni.h> #include <android/native_window.h> #include "include/yuv/libyuv.h" JNIEXPORT void JNICALL Java_com_haohao_ffmpeg_AVUtils_videoRender(JNIEnv *env, jclass type, jstring input_, jobject surface) { //需要转码的视频文件(输入的视频文件) const char *input = (*env)->GetStringUTFChars(env, input_, 0); //注册所有组件 av_register_all(); //avcodec_register_all(); //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息 AVFormatContext *pFormatCtx = avformat_alloc_context(); //打开输入视频文件 if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) { LOGE("%s", "无法打开输入视频文件"); return; } //获取视频文件信息,例如得到视频的宽高 //第二个参数是一个字典,表示你需要获取什么信息,比如视频的元数据 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { LOGE("%s", "无法获取视频文件信息"); return; } //获取视频流的索引位置 //遍历所有类型的流(音频流、视频流、字幕流),找到视频流 int v_stream_idx = -1; int i = 0; //number of streams for (; i < pFormatCtx->nb_streams; i++) { //流的类型 if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } } if (v_stream_idx == -1) { LOGE("%s", "找不到视频流\n"); return; } //获取视频流中的编解码上下文 AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec; //根据编解码上下文中的编码 id 查找对应的解码器 AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { LOGE("%s", "找不到解码器,或者视频已加密\n"); return; } //打开解码器,解码器有问题(比如说我们编译FFmpeg的时候没有编译对应类型的解码器) if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { LOGE("%s", "解码器无法打开\n"); return; } //准备读取 //AVPacket用于存储一帧一帧的压缩数据(H264) //缓冲区,开辟空间 AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket)); //AVFrame用于存储解码后的像素数据(YUV) //内存分配 AVFrame *yuv_frame = av_frame_alloc(); AVFrame *rgb_frame = av_frame_alloc(); int got_picture, ret; int frame_count = 0; //窗体 ANativeWindow *pWindow = ANativeWindow_fromSurface(env, surface); //绘制时的缓冲区 ANativeWindow_Buffer out_buffer; //一帧一帧的读取压缩数据 while (av_read_frame(pFormatCtx, packet) >= 0) { //只要视频压缩数据(根据流的索引位置判断) if (packet->stream_index == v_stream_idx) { //7.解码一帧视频压缩数据,得到视频像素数据 ret = avcodec_decode_video2(pCodecCtx, yuv_frame, &got_picture, packet); if (ret < 0) { LOGE("%s", "解码错误"); return; } //为0说明解码完成,非0正在解码 if (got_picture) { //lock window //设置缓冲区的属性:宽高、像素格式(需要与Java层的格式一致) ANativeWindow_setBuffersGeometry(pWindow, pCodecCtx->width, pCodecCtx->height, WINDOW_FORMAT_RGBA_8888); ANativeWindow_lock(pWindow, &out_buffer, NULL); //初始化缓冲区 //设置属性,像素格式、宽高 //rgb_frame的缓冲区就是Window的缓冲区,同一个,解锁的时候就会进行绘制 avpicture_fill((AVPicture *) rgb_frame, out_buffer.bits, AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height); //YUV格式的数据转换成RGBA 8888格式的数据, FFmpeg 也可以转换,但是存在问题,使用libyuv这个库实现 I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0], yuv_frame->data[2], yuv_frame->linesize[2], yuv_frame->data[1], yuv_frame->linesize[1], rgb_frame->data[0], rgb_frame->linesize[0], pCodecCtx->width, pCodecCtx->height); //3、unlock window ANativeWindow_unlockAndPost(pWindow); frame_count++; LOGI("解码绘制第%d帧", frame_count); } } //释放资源 av_free_packet(packet); } av_frame_free(&yuv_frame); avcodec_close(pCodecCtx); avformat_free_context(pFormatCtx); (*env)->ReleaseStringUTFChars(env, input_, input); } #include "libswresample/swresample.h" #define MAX_AUDIO_FRME_SIZE 48000 * 4 //音频解码(重采样) JNIEXPORT void JNICALL Java_com_haohao_ffmpeg_AVUtils_audioDecode(JNIEnv *env, jclass type, jstring input_, jstring output_) { //访问静态方法 jmethodID mid = (*env)->GetStaticMethodID(env, type, "onNativeCallback", "()V"); const char *input = (*env)->GetStringUTFChars(env, input_, 0); const char *output = (*env)->GetStringUTFChars(env, output_, 0); //注册组件 av_register_all(); AVFormatContext *pFormatCtx = avformat_alloc_context(); //打开音频文件 if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) { LOGI("%s", "无法打开音频文件"); return; } //获取输入文件信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { LOGI("%s", "无法获取输入文件信息"); return; } //获取音频流索引位置 int i = 0, audio_stream_idx = -1; for (; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream_idx = i; break; } } //获取解码器 AVCodecContext *codecCtx = pFormatCtx->streams[audio_stream_idx]->codec; AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id); if (codec == NULL) { LOGI("%s", "无法获取解码器"); return; } //打开解码器 if (avcodec_open2(codecCtx, codec, NULL) < 0) { LOGI("%s", "无法打开解码器"); return; } //压缩数据 AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket)); //解压缩数据 AVFrame *frame = av_frame_alloc(); //frame->16bit 44100 PCM 统一音频采样格式与采样率 SwrContext *swrCtx = swr_alloc(); //重采样设置参数 //输入的采样格式 enum AVSampleFormat in_sample_fmt = codecCtx->sample_fmt; //输出采样格式16bit PCM enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; //输入采样率 int in_sample_rate = codecCtx->sample_rate; //输出采样率 int out_sample_rate = 44100; //获取输入的声道布局 //根据声道个数获取默认的声道布局(2个声道,默认立体声stereo) //av_get_default_channel_layout(codecCtx->channels); uint64_t in_ch_layout = codecCtx->channel_layout; //输出的声道布局(立体声) uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO; swr_alloc_set_opts(swrCtx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout, in_sample_fmt, in_sample_rate, 0, NULL); swr_init(swrCtx); //输出的声道个数 int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout); //重采样设置参数 //位宽16bit 采样率 44100HZ 的 PCM 数据 uint8_t *out_buffer = (uint8_t *) av_malloc(MAX_AUDIO_FRME_SIZE); FILE *fp_pcm = fopen(output, "wb"); int got_frame = 0, index = 0, ret; //不断读取压缩数据 while (av_read_frame(pFormatCtx, packet) >= 0) { //解码 ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, packet); if (ret < 0) { LOGI("%s", "解码完成"); } //解码一帧成功 if (got_frame > 0) { LOGI("解码:%d", index++); swr_convert(swrCtx, &out_buffer, MAX_AUDIO_FRME_SIZE, frame->data, frame->nb_samples); //获取sample的size int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb, frame->nb_samples, out_sample_fmt, 1); fwrite(out_buffer, 1, out_buffer_size, fp_pcm); } av_free_packet(packet); } fclose(fp_pcm); av_frame_free(&frame); av_free(out_buffer); swr_free(&swrCtx); avcodec_close(codecCtx); avformat_close_input(&pFormatCtx); (*env)->ReleaseStringUTFChars(env, input_, input); (*env)->ReleaseStringUTFChars(env, output_, output); //通知 Java 层解码完成 (*env)->CallStaticVoidMethod(env, type, mid); } JNIEXPORT void JNICALL Java_com_haohao_ffmpeg_AVUtils_audioPlay(JNIEnv *env, jclass type, jstring input_) { const char *input = (*env)->GetStringUTFChars(env, input_, 0); LOGI("%s", "sound"); //注册组件 av_register_all(); AVFormatContext *pFormatCtx = avformat_alloc_context(); //打开音频文件 if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) { LOGI("%s", "无法打开音频文件"); return; } //获取输入文件信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { LOGI("%s", "无法获取输入文件信息"); return; } //获取音频流索引位置 int i = 0, audio_stream_idx = -1; for (; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream_idx = i; break; } } //获取解码器 AVCodecContext *codecCtx = pFormatCtx->streams[audio_stream_idx]->codec; AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id); if (codec == NULL) { LOGI("%s", "无法获取解码器"); return; } //打开解码器 if (avcodec_open2(codecCtx, codec, NULL) < 0) { LOGI("%s", "无法打开解码器"); return; } //压缩数据 AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket)); //解压缩数据 AVFrame *frame = av_frame_alloc(); //frame->16bit 44100 PCM 统一音频采样格式与采样率 SwrContext *swrCtx = swr_alloc(); //输入的采样格式 enum AVSampleFormat in_sample_fmt = codecCtx->sample_fmt; //输出采样格式16bit PCM enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; //输入采样率 int in_sample_rate = codecCtx->sample_rate; //输出采样率 int out_sample_rate = in_sample_rate; //获取输入的声道布局 //根据声道个数获取默认的声道布局(2个声道,默认立体声stereo) //av_get_default_channel_layout(codecCtx->channels); uint64_t in_ch_layout = codecCtx->channel_layout; //输出的声道布局(立体声) uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO; swr_alloc_set_opts(swrCtx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout, in_sample_fmt, in_sample_rate, 0, NULL); swr_init(swrCtx); //输出的声道个数 int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout); //AudioTrack对象 jmethodID create_audio_track_mid = (*env)->GetStaticMethodID(env, type, "createAudioTrack", "(II)Landroid/media/AudioTrack;"); jobject audio_track = (*env)->CallStaticObjectMethod(env, type, create_audio_track_mid, out_sample_rate, out_channel_nb); //调用AudioTrack.play方法 jclass audio_track_class = (*env)->GetObjectClass(env, audio_track); jmethodID audio_track_play_mid = (*env)->GetMethodID(env, audio_track_class, "play", "()V"); jmethodID audio_track_stop_mid = (*env)->GetMethodID(env, audio_track_class, "stop", "()V"); (*env)->CallVoidMethod(env, audio_track, audio_track_play_mid); //AudioTrack.write jmethodID audio_track_write_mid = (*env)->GetMethodID(env, audio_track_class, "write", "([BII)I"); //16bit 44100 PCM 数据 uint8_t *out_buffer = (uint8_t *) av_malloc(MAX_AUDIO_FRME_SIZE); int got_frame = 0, index = 0, ret; //不断读取压缩数据 while (av_read_frame(pFormatCtx, packet) >= 0) { //解码音频类型的Packet if (packet->stream_index == audio_stream_idx) { //解码 ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, packet); if (ret < 0) { LOGI("%s", "解码完成"); } //解码一帧成功 if (got_frame > 0) { LOGI("解码:%d", index++); swr_convert(swrCtx, &out_buffer, MAX_AUDIO_FRME_SIZE, (const uint8_t **) frame->data, frame->nb_samples); //获取sample的size int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb, frame->nb_samples, out_sample_fmt, 1); //out_buffer缓冲区数据,转成byte数组 jbyteArray audio_sample_array = (*env)->NewByteArray(env, out_buffer_size); jbyte *sample_bytep = (*env)->GetByteArrayElements(env, audio_sample_array, NULL); //out_buffer的数据复制到sampe_bytep memcpy(sample_bytep, out_buffer, out_buffer_size); //同步 (*env)->ReleaseByteArrayElements(env, audio_sample_array, sample_bytep, 0); //AudioTrack.write PCM数据 (*env)->CallIntMethod(env, audio_track, audio_track_write_mid, audio_sample_array, 0, out_buffer_size); //释放局部引用 (*env)->DeleteLocalRef(env, audio_sample_array); } } av_free_packet(packet); } (*env)->CallVoidMethod(env, audio_track, audio_track_stop_mid); av_frame_free(&frame); av_free(out_buffer); swr_free(&swrCtx); avcodec_close(codecCtx); avformat_close_input(&pFormatCtx); (*env)->ReleaseStringUTFChars(env, input_, input); }
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1) include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include) set(jnilibs "${CMAKE_SOURCE_DIR}/src/main/jniLibs") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${jnilibs}/${ANDROID_ABI}) add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/native-lib.c) # 添加 FFmpeg 的 8 个函数库和 yuvlib 库 add_library(avutil-54 SHARED IMPORTED ) set_target_properties(avutil-54 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavutil-54.so") add_library(swresample-1 SHARED IMPORTED ) set_target_properties(swresample-1 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libswresample-1.so") add_library(avcodec-56 SHARED IMPORTED ) set_target_properties(avcodec-56 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavcodec-56.so") add_library(avformat-56 SHARED IMPORTED ) set_target_properties(avformat-56 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavformat-56.so") add_library(swscale-3 SHARED IMPORTED ) set_target_properties(swscale-3 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libswscale-3.so") add_library(postproc-53 SHARED IMPORTED ) set_target_properties(postproc-53 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libpostproc-53.so") add_library(avfilter-5 SHARED IMPORTED ) set_target_properties(avfilter-5 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavfilter-5.so") add_library(avdevice-56 SHARED IMPORTED ) set_target_properties(avdevice-56 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavdevice-56.so") add_library(yuv SHARED IMPORTED ) set_target_properties(yuv PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libyuv.so") find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) #找到 Android 系统 Window 绘制相关的库 find_library( android-lib android ) target_link_libraries( native-lib ${log-lib} ${android-lib} avutil-54 swresample-1 avcodec-56 avformat-56 swscale-3 postproc-53 avfilter-5 avdevice-56 yuv)
PS:
- 注意添加文件读写权限。
- 关注公众号
字节流动
,并在后台回复ffmpeglib
获取相应的函数库。
参考文章
雷霄骅博客 http://blog.csdn.net/leixiaohua1020/article/details/15811977
Jason 的 NDK 开发高级教程
博客 NDK 开发系列文章:
- NDK 编译的三种方式
- NDK 开发中引入第三方静态库和动态库
- NDK 开发中 Native 与 Java 交互
- NDK POSIX 多线程编程
- NDK Android OpenSL ES 音频采集与播放
- NDK FFmpeg 编译
- NDK FFmpeg 音视频解码
- NDK 直播流媒体服务器搭建
- NDK 直播推流与引流
- NDK 开发中快速定位 Crash 问题
「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。