本系列文章由刀码旦编写,转载请注明出处
引言
本ffmpeg系列文章可以看作是基于雷神的博客的学习实践,由于雷神所使用的ffmpeg是老版本,一些地方我也会根据新版本的API做了一些更新,另外改为了Cmake构建方式,这也是区别于雷神博客的地方。
本文记录一个安卓平台下基于FFmpeg的视频解码器,目的是将一段视频文件解码为YUV数据。
至于YUV是什么,可以参考下这篇文章
,暂且简单理解就可以。
准备工作
1.本文继续沿用上一篇中的项目Android平台基于ffmpeg的Helloworld。
2.手动创建了一个解码操作页面DecodecActivity 以及用来实现解码功能的c文件simple_ffmpeg_decoder.c,并配置CmakeLists.txt脚本。
3.准备一个mp4的测试视频,将视频导入到设备sdcard根目录下
这里提供一个视频下载
开始
项目结构如下
image.png
有别于之前,这里手动创建了一个解码操作页面DecodecActivity 以及用来实现解码功能的c文件simple_ffmpeg_decoder.c,以及
首先先用c语言实现解码功能,代码如下
simple_ffmpeg_decoder.c
//
// Created by ing on 2019/7/31.
//最简单的基于FFmpeg的视频解码器
#include <stdio.h>
#include <time.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/log.h"
#include "libavutil/imgutils.h"
#ifdef ANDROID
#include <jni.h>
#include <android/log.h>
#include <libavformat/avformat.h>
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR,"(>_<)",format,##__VA_ARGS__)
#define LOGI(format, ...) __android_log_print(ANDROID_LOG_INFO,"(^_^)",format,##__VA_ARGS__)
#else
#define LOGE(format, ...) printf("(>_<) " format "\n", ##__VA_ARGS__)
#define LOGI(format, ...) printf("(^_^) " format "\n", ##__VA_ARGS__)
#endif
//Output FFmpeg's av_log()
void custom_log(void *ptr, int level, const char *fmt, va_list vl) {
FILE *fp = fopen("/storage/emulated/0/av_log.txt", "a+");
if (fp) {
vfprintf(fp, fmt, vl);
fflush(fp);
fclose(fp);
}
}
JNIEXPORT jint JNICALL
Java_com_ing_ffmpeg_DecodecActivity_decode(JNIEnv *env, jobject obj, jstring input_jstr,
jstring output_jstr) {
AVFormatContext *pFormatCtx;
int i, videoindex;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVFrame *pFrame, *pFrameYUV;
uint8_t *out_buffer;
AVPacket *packet;
int y_size;
int ret, got_picture;
struct SwsContext *img_convert_ctx;
FILE *fp_yuv;
int frame_cnt;
clock_t time_start, time_finish;
double time_duration = 0.0;
char input_str[500] = {0};
char output_str[500] = {0};
char info[1000] = {0};
sprintf(input_str, "%s", (*env)->GetStringUTFChars(env, input_jstr, NULL));
sprintf(output_str, "%s", (*env)->GetStringUTFChars(env, output_jstr, NULL));
av_log_set_callback(custom_log);
av_register_all();
avformat_network_init();
pFormatCtx = avformat_alloc_context();
/**
* 打开音视频文件 avformat_open_input 主要负责服务器的连接和码流头部信息的拉取
* 函数读取媒体文件的文件头并将文件格式相关的信息存储AVFormatContext上下文中。
* 第二参数 input_str 文件的路径
* 第三参数 用于指定媒体文件格式
* 第四参数 文件格式的相关选项
* 后面两个参数如果传入的NULL,那么libavformat将自动探测文件格式
**/
if (avformat_open_input(&pFormatCtx, input_str, NULL, NULL) != 0) {
LOGE("Couldn't open input stream.\n");
return -1;
}
/**
*
* 媒体信息的探测和分析
* 函数会为pFormatCtx->streams填充对应的信息
*
*
* AVFormatContext 里包含了下面这些跟媒体信息有关的成员:
----------AVFormatContext-------
struct AVInputFormat *iformat; // 记录了封装格式信息
unsigned int nb_streams; // 记录了该 URL 中包含有几路流
AVStream **streams; // 一个结构体数组,每个对象记录了一路流的详细信息
int64_t start_time; // 第一帧的时间戳
int64_t duration; // 码流的总时长
int64_t bit_rate; // 码流的总码率,bps
AVDictionary *metadata; // 一些文件信息头,key/value 字符串
pFormatCtx->streams是一个AVStream指针的数组,里面包含了媒体资源的每一路流信息,数组大小为pFromatCtx->nb_streams
--------AVStream---------
AVStream 结构体中关键的成员包括:
AVCodecContext *codec; // 记录了该码流的编码信息
int64_t start_time; // 第一帧的时间戳
int64_t duration; // 该码流的时长
int64_t nb_frames; // 该码流的总帧数
AVDictionary *metadata; // 一些文件信息头,key/value 字符串
AVRational avg_frame_rate; // 平均帧率
----------AVCodecContext---------
AVCodecContext 则记录了一路流的具体编码信息,其中关键的成员包括:
const struct AVCodec *codec; // 编码的详细信息
enum AVCodecID codec_id; // 编码类型
int bit_rate; // 平均码率
video only:
int width, height; // 图像的宽高尺寸,码流中不一定存在该信息,会由解码后覆盖
enum AVPixelFormat pix_fmt; // 原始图像的格式,码流中不一定存在该信息,会由解码后覆盖
audio only:
int sample_rate; // 音频的采样率
int channels; // 音频的通道数
enum AVSampleFormat sample_fmt; // 音频的格式,位宽
int frame_size; // 每个音频帧的 sample 个数
*/
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
LOGE("Couldn't find stream information.\n");
return -1;
}
videoindex = -1;
for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
if (pFormatCtx->streams[i]/*音视频流*/->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)//查找视频流
{
videoindex = i;
break;
}
}
if (videoindex == -1) {
LOGE("Couldn't find a video stream.\n");
return -1;
}
/**
* pCodecCtx = pFormatCtx->streams[videoindex]->codec;//指向AVCodecContext的指针 #已废弃,不赞成使用。
*/
pCodecCtx = avcodec_alloc_context3(NULL);
if (pCodecCtx == NULL) {
printf("Could not allocate AVCodecContext\n");
return -1;
}
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar);
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);//指向AVCodec的指针,查找解码器
if (pCodec == NULL) {
LOGE("Couldn't find Codec.\n");
return -1;
}
//打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
LOGE("Couldn't open codec.\n");
return -1;
}
/**------存储数据 存储视频的帧 并转化格式------**/
pFrame = av_frame_alloc();
pFrameYUV = av_frame_alloc();
//当转换格式时,我们需要一块内存来存储视频帧的原始数据。
// 为已经分配空间的结构体AVPicture挂上一段用于保存数据的空间
// AVFrame/AVPicture有一个data[4]的数据字段,buffer里面存放的只是yuv这样排列的数据,
// 而经过fill 之后,会把buffer中的yuv分别放到data[0],data[1],data[2]中。av_image_get_buffer_size来获取需要的内存大小,然后手动分配这块内存。
out_buffer = (unsigned char *) av_malloc(
av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1));
//关联frame和我们刚才分配的内存---存储视频帧的原始数据
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer, AV_PIX_FMT_YUV420P,
pCodecCtx->width, pCodecCtx->height, 1);
/**----------------读取数据----------------**/
packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//初始化一个SwsContext 图形裁剪
//参数 源图像的宽,源图像的高,源图像的像素格式,目标图像的宽,目标图像的高,目标图像的像素格式,设定图像拉伸使用的算法
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
sprintf(info, "[Input ]$s\n", input_str);
sprintf(info, "%s[Output ]%s\n", info, output_str);
sprintf(info, "%s[Format ]%s\n", info, pFormatCtx->iformat->name);
sprintf(info, "%s[Codec ]%s]\n", info, pCodecCtx->codec->name);
sprintf(info, "%s[Resolution ]%dx%d\n", info, pCodecCtx->width, pCodecCtx->height);
fp_yuv = fopen(output_str, "wb+");
if (fp_yuv == NULL) {
printf("Cannot open output file.\n");
return -1;
}
frame_cnt = 0;
time_start = clock();
while (av_read_frame(pFormatCtx, packet) >= 0) {
if (packet->stream_index == videoindex) {
//解码一帧视频数据,输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) {
LOGE("Decode Error.\n");
return -1;
}
got_picture = avcodec_receive_frame(pCodecCtx, pFrame);
if (got_picture) {
//转换像素
//解码后yuv格式的视频像素数据保存在AVFrame的data[0]、data[1]、data[2]中。
// 但是这些像素值并不是连续存储的,每行有效像素之后存储了一些无效像素
// ,以高度Y数据为例,data[0]中一共包含了linesize[0]*height个数据。
// 但是出于优化等方面的考虑,linesize[0]实际上并不等于宽度width,而是一个比宽度大一些的值。
// 因此需要使用ses_scale()进行转换。转换后去除了无效数据,width和linesize[0]就取值相同了
sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize,
0,
pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
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[1], 1, y_size / 4, fp_yuv);
//Output info
char pictype_str[10] = {0};
switch (pFrame->pict_type) {
case AV_PICTURE_TYPE_I:
sprintf(pictype_str, "I");
break;
case AV_PICTURE_TYPE_P:
sprintf(pictype_str, "p");
break;
case AV_PICTURE_TYPE_B:
sprintf(pictype_str, "B");
break;
default:
sprintf(pictype_str, "Other");
break;
}
LOGI("Frame Index : %5d.Type:%s", frame_cnt, pictype_str);
frame_cnt++;
}
}
// av_free_packet(packet);已废弃
av_packet_unref(packet);
}
//flush_decoder
//当av_read_frame()循环退出时,实际上解码器中可能还包含剩余的几帧数据,因此需要通过flush_decoder将这几帧数据输出。
//flush_decoder功能简而言之即直接调用avcodec_send_packet()获得AVFrame,而不再向解码器传递AVPacket
while (1) {
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) {
break;
}
if (!got_picture) {
break;
}
sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize, 0,
pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
int y_size = pCodecCtx->width * pCodecCtx->height;
fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);//y
fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);//u
fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);//v
//Output info
char pictype_str[10] = {0};
switch (pFrame->pict_type) {
case AV_PICTURE_TYPE_I:
sprintf(pictype_str, "I");
break;
case AV_PICTURE_TYPE_P:
sprintf(pictype_str, "p");
break;
case AV_PICTURE_TYPE_B:
sprintf(pictype_str, "B");
break;
default:
sprintf(pictype_str, "Other");
break;
}
LOGI("Frame Index:%5d. Type:%s", frame_cnt, pictype_str);
frame_cnt++;
}
time_finish = clock();
time_duration = (double) (time_finish - time_start);
sprintf(info, "%s[Time ]%fms\n", info, time_duration);
sprintf(info, "%s[Count ]%d\n", info, frame_cnt);
fclose(fp_yuv);
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
LOGI("%s", "解码完成.");
return 0;
}
代码中依据自己的理解以及资料做了一部分注释,自己也没有完全吃透,所以仅供参考,如有纰漏,欢迎指正。
至于DecodecActivity 就直接贴代码了
DecodecActivity
package com.ing.ffmpeg;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
/**
* Created by ing on 2019/8/1
*/
public class DecodecActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_decodec);
Button start = findViewById(R.id.start);
final EditText edt_input = findViewById(R.id.edt_input);
final EditText edt_output = findViewById(R.id.edt_output);
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String folderUrl = Environment.getExternalStorageDirectory().getPath();
String urlinput = folderUrl+"/"+edt_input.getText().toString();
String urloutput = folderUrl+"/"+edt_output.getText().toString();
Log.i("Url","input url ="+urlinput);
Log.i("Url","output url ="+urloutput);
decode(urlinput,urloutput);
}
});
}
//JNI
public native int decode(String inputurl, String outputurl);
}
对应的xml布局文件activit_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Input Bitstream" />
<EditText
android:id="@+id/edt_input"
android:layout_width="match_parent"
app:layout_constraintTop_toBottomOf="@id/tv1"
android:layout_marginTop="10dp"
android:hint="原视频文件名"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"/>
<TextView
android:id="@+id/tv2"
app:layout_constraintTop_toBottomOf="@+id/edt_input"
android:layout_width="wrap_content"
android:layout_marginTop="10dp"
android:layout_height="wrap_content"
android:text="Output Raw YUV" />
<EditText
android:id="@+id/edt_output"
app:layout_constraintTop_toBottomOf="@id/tv2"
android:layout_width="match_parent"
android:hint="解码后的存储文件名"
android:layout_height="wrap_content"
android:layout_margin="20dp"/>
<Button
android:id="@+id/start"
app:layout_constraintTop_toBottomOf="@+id/edt_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start"/>
</android.support.constraint.ConstraintLayout>
另外就是MainActivity里增加一个按钮,简单实现跳转即可
image.png
当然别忘了AndroidManifest.xml里注册下DecodecActivity
<activity android:name=".DecodecActivity" android:screenOrientation="portrait"/>
最最最后,千万不要忘了配置CMakeLists.txt,将新加的c源码添加到库native-lib中
然后【build】--【make project】完成so的编译,编译成功也就说明native-lib.so已经存在解码功能了。
运行顺利的话,进入MainActivity点击【解码示例】按钮,进入DecodecActivity
device-2019-08-08-170943.png
输入你的视频名,解码后保存的文件名,然后【start】即可,可能需要等待一会,解码完成时,日志会打印"(_):解码完成."此时sdcard下会生成一个xxx.yuv文件
image.png
就说明成功了。
网友评论