FFMpegDemo
ffmpeg play video from android
1 利用FFMpeg進行MP3視頻轉YUV格式
理論:
YUV,是一種顏色編碼方法
詳細看這裏
https://blog.csdn.net/junzia/article/details/76315120
爲什麼需要轉yuv格式
現在絕大多數視頻解碼後播放的格式都是YUV 所以需要做下YUV格式
一個通道.
前面放Y 後面放UV 比例是 4:1:1
視頻yuv 音頻 是pcm
YUV
- “Y”表示明亮度(Luminance、Luma),“U”和“V”則是色度、濃度(Chrominance、Chroma)。
封裝格式: MP4 rmvb等
編碼格式: 對應響應的編碼 MPEG2,MPEG4,H.264
視頻播放一般有兩個 流 音頻流 視頻流, 有時還有個一流 是字幕流
下面開始預先操作
- 把ffmpeg 的so庫 還有頭文件導入到android studio 工程中去.
- 在native-lib中導入頭文件.
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
}
- 在手機根目錄放個mp4視頻
視頻信息
可以看到:
- 封裝格式mp4
- 編碼格式avc1格式
大致流程圖:
代碼
註釋都有
#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>
//extern "C" 主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼 加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
}
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);
JNICALL extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_MainActivity_openVideo(JNIEnv *env, jobject instance, jstring inputStr_,
jstring outStr_) {
const char *inputStr = env->GetStringUTFChars(inputStr_, 0);
const char *outStr = env->GetStringUTFChars(outStr_, 0);
//無論編碼還是解碼 都要調用這個 註冊各大組件
av_register_all();
//獲取AVFormatContext 比特率 時長 文件路徑 流的信息(nustream) 都封裝在這裏面
AVFormatContext *pContext = avformat_alloc_context();
//AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
//上下文 文件名 打開文件格式 獲取信息(AVDictionary) 凡是AVDictionary字典 都是獲取視頻文件信息
if (avformat_open_input(&pContext, inputStr, NULL, NULL) < 0) {
LOGE("打開失敗");
return;
}
//給nbstram填充信息
if (avformat_find_stream_info(pContext, NULL) < 0) {
LOGE("獲取信息失敗");
return;
}
//找到視頻流
int video_stream_ids = -1;
for (int i = 0; i < pContext->nb_streams; ++i) {
LOGE("循環 %d", i);
//如果填充的視頻流信息 -> 編解碼,解碼器 -> 流的類型 == 視頻的類型
//codec 每一個流 對應的解碼上下文 codec_type 流的類型
if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_ids = i;
}
}
// 獲取到解碼器上下文
AVCodecContext *pCodecCtx = pContext->streams[video_stream_ids]->codec;
//解碼器
AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
//打開解碼器 爲什麼avcodec_open2 版本升級的原因
if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
LOGE("解碼失敗");
return;
}
//得到解封裝 讀取 解封每一幀 讀取每一幀的壓縮數據
//初始化avpacket 分配內存 FFMpeg 沒有自動分配內存 必須手動分匹配手動釋放 不過有分配函數
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//初始化packet 每一個包是一個完整的數據幀,來暫存解複用之後、解碼之前的媒體數據(一個音/視頻幀、一個字幕包等)及附加信息(解碼時間戳、顯示時間戳、時長等)
av_init_packet(packet);
//初始化AVFrame
AVFrame *frame = av_frame_alloc();
//目的數據 YUV的frame 聲明yuvFrame
AVFrame *yuvFrame = av_frame_alloc();
//給yuv 緩衝區初始化
//avpicture_get_size 計算給定寬度和高度的圖片的字節大小 如果以給定的圖片格式存儲。
// uint8_t 8位無符號整型數(int)
uint8_t *out_buffer = (uint8_t *) av_malloc(
avpicture_get_size((AV_PIX_FMT_YUV420P), pCodecCtx->width, pCodecCtx->height));
//填充YUM緩衝區
int re = avpicture_fill(reinterpret_cast<AVPicture *>(yuvFrame), out_buffer, AV_PIX_FMT_YUV420P,
pCodecCtx->width, pCodecCtx->height);
LOGE("寬 %d 高 %d",pCodecCtx->width,pCodecCtx->height);
//獲取轉化的上下文 參數 通過解碼器的上下文獲取到 pCodecCtx->pix_fmt 是mp4的上下文
SwsContext *swsContext = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
int frame_count = 0;
//目的轉化成YUV 寫到一個文件裏面去 聲明文件
FILE *fp_yuv = fopen(outStr, "wb");
//AVFormatContext *s, AVPacket *pkt 上下文,avpacket 是數據包的意思
//packet 入參
int got_frame;
//解碼每一幀圖片
while (av_read_frame(pContext, packet) >= 0) {
// 解封裝
//參數
//AVCodecContext *avctx, 解碼器的上下文
// AVFrame *picture, 已經解封裝的frame,是直接能在手機上播放的frame
// int *got_picture_ptr, 入參出參對象, 可以根據出參判斷是否解析完成
// const AVPacket *avpkt 壓縮的packet數據
//把packet壓縮的數據 解壓 賦給frame
avcodec_decode_video2(pCodecCtx, frame, &got_frame, packet);
//判斷是否讀完
if (got_frame > 0) {
LOGE("解碼第 %d 個 frame ",frame_count++);
// 獲取到了frame數據 視頻像素的數據
// 目的轉化爲yuv格式 sws_scale 函數
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0, frame->height, yuvFrame->data, yuvFrame->linesize);
//得到所有的大小 寬度乘以高度 解釋: 在R G B 中有多少像素 就是寬度乘以高度, 在YUV中 有多少像素是由Y決定的 如果只有Y 那麼只有亮度 就是黑白的
int y_size = pCodecCtx->width * pCodecCtx->height;
//Y亮度信息
fwrite(yuvFrame->data[0], 1, y_size, fp_yuv);
//色度
fwrite(yuvFrame->data[1], 1, y_size / 4, fp_yuv);
//濃度
fwrite(yuvFrame->data[2], 1, y_size / 4, fp_yuv);
}
//釋放
av_free_packet(packet);
}
fclose(fp_yuv);
av_frame_free(&yuvFrame);
av_frame_free(&frame);
avcodec_close(pCodecCtx);
avformat_free_context(pContext);
env->ReleaseStringUTFChars(inputStr_, inputStr);
env->ReleaseStringUTFChars(outStr_, outStr);
}
結果
- sws_getContext 函數解析
// 初始化sws_scale
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags,
SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param);
參數int srcW, int srcH, enum AVPixelFormat srcFormat定義輸入圖像信息(寬、高、顏色空間(像素格式))
參數int dstW, int dstH, enum AVPixelFormat dstFormat定義輸出圖像信息寬、高、顏色空間(像素格式))。
參數int flags選擇縮放算法(只有當輸入輸出圖像大小不同時有效)
參數SwsFilter *srcFilter, SwsFilter *dstFilter分別定義輸入/輸出圖像濾波器信息,如果不做前後圖像濾波,輸入NULL
參數const double *param定義特定縮放算法需要的參數(?),默認爲NULL
函數返回SwsContext結構體,定義了基本變換信息。
如果是對一個序列的所有幀做相同的處理,函數sws_getContext只需要調用一次就可以了。
sws_getContext(w, h, YV12, w, h, NV12, 0, NULL, NULL, NULL); // YV12->NV12 色彩空間轉換
sws_getContext(w, h, YV12, w/2, h/2, YV12, 0, NULL, NULL, NULL); // YV12圖像縮小到原圖1/4
sws_getContext(w, h, YV12, 2w, 2h, YN12, 0, NULL, NULL, NULL); // YV12圖像放大到原圖4倍,並轉換爲NV12結構
sws_scale
縮放SRCPLACE中的圖像切片並將其縮放
需要注意的是第四個參數srcSliceY 這個代表的是第一列要處理的位置,如果要從頭開始處理,直接填0即可
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY,int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
*
-C以前創建的縮放上下文
* SWSYGETCONTRONTHECT()
*@ PARAM SRCPLACE包含數組指向平面的指針
*源切片
*@ PARAM SrcReST包含每個平面的步幅的數組
*源圖像
*@ PARAM SRCLSICY在切片的源圖像中的位置
*進程,即數字(從開始計數)
*0)在切片的第一行的圖像中
*@ PARAM SRCLSICH源數組的高度,即
*切片中的行
*@ PARAM DST包含指向平面的指針的數組
*目的地形象
*@ PARAM-DSSTRIDE包含每個平面的步長的數組
*目的地形象
*@返回輸出條的高度
利用surfaceview播放
理論
解碼部分和上面的代碼基本上差不多.解碼出來會得到了rgbframe (和yuvframe一樣 不過格式不是YUV格式,因爲surfaceview不能識別YUV,只能識別RGB.).
主要問題是解碼後如何給surfaceview播放. ? c++ 裏面是利用 ANativeWindow 播放. ANativeWindow 播放需要一個緩衝區buffer.
這個buffer 可以通過rgbframe 一行一行的寫入緩衝區(ANativeWindow的緩衝區).
導入頭文件:
#include <android/native_window_jni.h> 導入頭文件後需要導入so庫實現, 在cmakelists裏面加上 可以和log 庫導入一樣,使用find_library,也可以直接在target_link_libraries加上android
#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>
//extern "C" 主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼 加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
//用於android 繪製圖像的
#include <android/native_window_jni.h>
#include <unistd.h>
}
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);
extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_MyVideoView_render(JNIEnv *env, jobject instance, jstring input_,
jobject surface) {
const char *input = env->GetStringUTFChars(input_, 0);
// TODO
//無論編碼還是解碼 都要調用這個 註冊各大組件
av_register_all();
//獲取AVFormatContext 比特率 時長 文件路徑 流的信息(nustream) 都封裝在這裏面
AVFormatContext *pContext = avformat_alloc_context();
//AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
//上下文 文件名 打開文件格式 獲取信息(AVDictionary) 凡是AVDictionary字典 都是獲取視頻文件信息
if (avformat_open_input(&pContext, input, NULL, NULL) < 0) {
LOGE("打開失敗");
return;
}
//給nbstram填充信息
if (avformat_find_stream_info(pContext, NULL) < 0) {
LOGE("獲取信息失敗");
return;
}
//找到視頻流
int video_stream_ids = -1;
for (int i = 0; i < pContext->nb_streams; ++i) {
LOGE("循環 %d", i);
//如果填充的視頻流信息 -> 編解碼,解碼器 -> 流的類型 == 視頻的類型
//codec 每一個流 對應的解碼上下文 codec_type 流的類型
if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_ids = i;
break;
}
}
// 獲取到解碼器上下文 獲取視頻編解碼器
AVCodecContext *pCodecCtx = pContext->streams[video_stream_ids]->codec;
//解碼器
AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
//打開解碼器 爲什麼avcodec_open2 版本升級的原因
if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
LOGE("解碼失敗");
return;
}
//得到解封裝 讀取 解封每一幀 讀取每一幀的壓縮數據
//初始化avpacket 分配內存 FFMpeg 沒有自動分配內存 必須手動分匹配手動釋放 不過有分配函數
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//初始化packet 每一個包是一個完整的數據幀,來暫存解複用之後、解碼之前的媒體數據(一個音/視頻幀、一個字幕包等)及附加信息(解碼時間戳、顯示時間戳、時長等)
// av_init_packet(packet);
//初始化AVFrame
AVFrame *frame = av_frame_alloc();
//目的數據 RGB的frame 聲明RGBFrame
AVFrame *rgbFrame = av_frame_alloc();
//給RGB 緩衝區初始化 分配內存
//avpicture_get_size 計算給定寬度和高度的圖片的字節大小 如果以給定的圖片格式存儲。
// uint8_t 8位無符號整型數(int)
uint8_t *out_buffer = (uint8_t *) av_malloc(
avpicture_get_size((AV_PIX_FMT_RGBA), pCodecCtx->width, pCodecCtx->height));
LOGE("render 寬 %d render 高 %d", pCodecCtx->width, pCodecCtx->height);
// //填充rgb緩衝區
int re = avpicture_fill(reinterpret_cast<AVPicture *>(rgbFrame), out_buffer, AV_PIX_FMT_RGBA,
pCodecCtx->width, pCodecCtx->height);
LOGE("申請YUM內存%d ", re);
//獲取轉化的上下文 參數 通過解碼器的上下文獲取到 pCodecCtx->pix_fmt 是mp4的上下文 SWS_BICUBIC 是級別 越往下效率越高清晰度越低
//AV_PIX_FMT_RGBA 這個是需要轉化的格式
SwsContext *swsContext = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGBA,
SWS_BICUBIC, NULL, NULL, NULL);
//AVFormatContext *s, AVPacket *pkt 上下文,avpacket 是數據包的意思
//packet 入參
int got_frame;
int length = 0;
//獲取ANtiviewindow ANativeWindow_fromSurface
ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
//聲明buffer 視頻的緩衝區 rgbFrame 的緩衝區
ANativeWindow_Buffer outBuffer;
int frame_count = 0;
//解碼每一幀圖片
while (av_read_frame(pContext, packet) >= 0) {
// 解封裝
//參數
//AVCodecContext *avctx, 解碼器的上下文
// AVFrame *picture, 已經解封裝的frame,是直接能在手機上播放的frame
// int *got_picture_ptr, 入參出參對象, 可以根據出參判斷是否解析完成
// const AVPacket *avpkt 壓縮的packet數據
if (packet->stream_index == video_stream_ids) {
//把packet壓縮的數據 解壓 賦給默認frame
length = avcodec_decode_video2(pCodecCtx, frame, &got_frame, packet);
LOGE(" 獲得長度 %d ", length);
//判斷是否讀完
if (got_frame) {
LOGE("解碼第 %d 個 frame ", frame_count++);
//繪製之前 需要配置一些信息. 寬高 輸出的格式 可以修改 pCodecCtx->width 和height
ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height,
WINDOW_FORMAT_RGBA_8888);//AV_PIX_FMT_RGBA 這個也是上面轉化的格式
//因爲surfaceview 只支持RGB所以 這裏不轉成YUV, 電視機 機頂盒都是YUV
//TODO buffer很重要
//先鎖定當前的window 和surfaceview 類似 需要先鎖定 , 第一個參數就是上面獲取的ANtiviewindow, 第二個參數是buffer 這個buffer非常重要他是需要繪製的緩衝區.
ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
// 調用轉化方式,目的轉化爲RGB格式 sws_scale 函數
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0, pCodecCtx->height, rgbFrame->data,
rgbFrame->linesize);
//獲取window 被輸出的畫面的首地址 window首地址
uint8_t *dst = static_cast<uint8_t *>(outBuffer.bits);
//拿到一行有多少字節 爲什麼需要×4 rgba
int destStride = outBuffer.stride * 4;//這裏就是window的一行數據 字節
//獲取rgbframe(像素數據)的 首地址
uint8_t *src = rgbFrame->data[0];
//實際內存一行的數量
int srcStride = rgbFrame->linesize[0];
LOGE("實際內存第一行一行的數量 = %d", srcStride);
//把rgbframe拷貝到緩衝區 到解碼的高度
for (int i = 0; i < pCodecCtx->height; ++i) {
//dest 目的地址 src 源地址 n 數量
//void *memcpy(void *dest, const void *src, size_t n);
//從源src所指的內存地址的起始位置 開始拷貝n個字節 到目標dest 所指的內存地址的起始位置中
//window首地址 變化的 真正拷貝的字節數 srcStride
memcpy(dst + i * destStride,
reinterpret_cast<const void *>(src + i * srcStride),
srcStride);
}//這個循環完成就完成拷貝了.
LOGE("解鎖畫布 睡眠16毫秒 ");
//解鎖畫布
ANativeWindow_unlockAndPost(nativeWindow);
//畫面繪製完之後 畫面停止16毫秒 usleep在#include <unistd.h> 中
usleep(1000 * 16);
}
}
//釋放
av_free_packet(packet);
}
ANativeWindow_release(nativeWindow);
av_frame_free(&rgbFrame);
av_frame_free(&frame);
avcodec_close(pCodecCtx);
avformat_free_context(pContext);
env->ReleaseStringUTFChars(input_, input);
}
結果: