###前言
上一篇文章我們對視訊進行了解碼,那麼這次我們隊解碼後的資料進行播放。也就是繪製到介面上。
###視訊播放
####建立自動以SurfaceView
因為視訊是需要快速實時重新整理介面的,因此要用到SurfaceView。
public class VideoView extends SurfaceView {
public VideoView(Context context) {
this(context, null, 0);
}
public VideoView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//初始化畫素繪製的格式為RGBA_8888(色彩最豐富)
SurfaceHolder holder = getHolder();
holder.setFormat(PixelFormat.RGBA_8888);
}
}
複製程式碼
這裡我們自定義了一個SurfaceView,指定輸出格式為RGBA_8888,這種格式色彩豐富度最高的。
然後新增到根佈局當中:
<?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.nan.ffmpeg.view.VideoView
android:id="@+id/sv_video"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btn_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放" />
</FrameLayout>
複製程式碼
####建立播放控制器類
public class VideoPlayer {
static {
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("avfilter-5");
System.loadLibrary("avdevice-56");
System.loadLibrary("ffmpeg-lib");
}
public native void render(String input, Surface surface);
}
複製程式碼
####native方法實現
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//畫素處理
#include "libswscale/swscale.h"
//使用這兩個Window相關的標頭檔案需要在CMake指令碼中引入android庫
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include "libyuv.h"
JNIEXPORT void JNICALL
Java_com_nan_ffmpeg_utils_VideoPlayer_render(JNIEnv *env, jobject instance, jstring input_,
jobject surface) {
//需要轉碼的視訊檔案(輸入的視訊檔案)
const char *input = (*env)->GetStringUTFChars(env, input_, 0);
//1.註冊所有元件,例如初始化一些全域性的變數、初始化網路等等
av_register_all();
//avcodec_register_all();
//封裝格式上下文,統領全域性的結構體,儲存了視訊檔案封裝格式的相關資訊
AVFormatContext *pFormatCtx = avformat_alloc_context();
//2.開啟輸入視訊檔案
if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
LOGE("%s", "無法開啟輸入視訊檔案");
return;
}
//3.獲取視訊檔案資訊,例如得到視訊的寬高
//第二個引數是一個字典,表示你需要獲取什麼資訊,比如視訊的後設資料
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;
//4.根據編解碼上下文中的編碼id查詢對應的解碼器
AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
//(迅雷看看,找不到解碼器,臨時下載一個解碼器,比如視訊加密了)
if (pCodec == NULL) {
LOGE("%s", "找不到解碼器,或者視訊已加密\n");
return;
}
//5.開啟解碼器,解碼器有問題(比如說我們編譯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;
//6.一幀一幀的讀取壓縮資料
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) {
//1、lock window
//設定緩衝區的屬性:寬高、畫素格式(需要與Java層的格式一致)
ANativeWindow_setBuffersGeometry(pWindow, pCodecCtx->width, pCodecCtx->height,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_lock(pWindow, &out_buffer, NULL);
//2、fix buffer
//初始化緩衝區
//設定屬性,畫素格式、寬高
//rgb_frame的緩衝區就是Window的緩衝區,同一個,解鎖的時候就會進行繪製
avpicture_fill((AVPicture *) rgb_frame, out_buffer.bits, AV_PIX_FMT_RGBA,
pCodecCtx->width,
pCodecCtx->height);
//YUV格式的資料轉換成RGBA 8888格式的資料
//FFmpeg可以轉,但是會有問題,因此我們使用libyuv這個庫來做
//https://chromium.googlesource.com/external/libyuv
//引數分別是資料、對應一行的大小
//I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0],
// yuv_frame->data[1], yuv_frame->linesize[1],
// yuv_frame->data[2], yuv_frame->linesize[2],
// rgb_frame->data[0], rgb_frame->linesize[0],
// pCodecCtx->width, pCodecCtx->height);
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);
}
複製程式碼
程式碼裡面需要注意的是,我們使用了yuvlib這個庫對YUV資料轉換為RGB資料。這個庫的下載地址是https://chromium.googlesource.com/external/libyuv。
修改Android.mk檔案最後一行,由輸出靜態庫改為輸出動態庫:
include $(BUILD_SHARED_LIBRARY)
複製程式碼
自己新建jni目錄,把所有檔案拷貝到裡面,Linux執行下面的命令編譯yuvlib:
ndk-build jni
複製程式碼
然後就會輸出預編譯的so庫,並且在Android Studio中使用了。CMake指令碼需要新增:
#YUV轉RGB需要的庫
add_library( yuv
SHARED
IMPORTED
)
set_target_properties(
yuv
PROPERTIES IMPORTED_LOCATION
${path_project}/app/libs/${ANDROID_ABI}/libyuv.so
)
複製程式碼
如果需要的話設定一些標頭檔案的包含路徑:
#配置標頭檔案的包含路徑
include_directories(${path_project}/app/src/main/cpp/include/yuv)
複製程式碼
最後在使用的時候包含對應的標頭檔案即可:
#include "libyuv.h"
複製程式碼
還有一個注意的地方就是我們要使用到視窗的原生繪製,那麼就需要引入window相關的標頭檔案:
//使用這兩個Window相關的標頭檔案需要在CMake指令碼中引入android庫
#include <android/native_window_jni.h>
#include <android/native_window.h>
複製程式碼
這些標頭檔案使用需要把android這個庫編譯進來,使用方法跟log庫一樣:
#找到Android的Window繪製相關的庫(這個庫已經在Android平臺中了)
find_library(
android-lib
android
)
複製程式碼
記得連結到自己的庫裡面:
#把需要的庫連結到自己的庫中
target_link_libraries(
ffmpeg-lib
${log-lib}
${android-lib}
avutil
swresample
avcodec
avformat
swscale
postproc
avfilter
avdevice
yuv
)
複製程式碼
####視窗的原生繪製流程
繪製需要一個surface物件。
原生繪製步驟:
- lock Window。
- 初始化緩衝區,設定大小,緩衝區賦值。
- 解鎖然後就繪製到視窗中了。
####測試
@Override
public void onClick(View v) {
String inputVideo = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar
+ "input.mp4";
switch (v.getId()) {
case R.id.btn_play:
sv_video = (SurfaceView) findViewById(R.id.sv_video);
//native方法中傳入SurfaceView的Surface物件,在底層進行繪製
mPlayer.render(inputVideo, sv_video.getHolder().getSurface());
break;
}
}
複製程式碼
如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:
我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)。