音視頻播放流程圖:

視頻播放原理解析:
視頻:視頻(Video)泛指將一系列靜態(tài)影像以電信號(hào)的方式加以捕捉、紀(jì)錄、處理、儲(chǔ)存、傳送與重現(xiàn)的各種技術(shù)。連續(xù)的圖像變化每秒超過(guò)24幀(frame)畫(huà)面以上時(shí),根據(jù)視覺(jué)暫留原理,人眼無(wú)法辨別單幅的靜態(tài)畫(huà)面;看上去是平滑連續(xù)的視覺(jué)效果,這樣連續(xù)的畫(huà)面叫做視頻。視頻技術(shù)最早是為了電視系統(tǒng)而發(fā)展,但現(xiàn)在已經(jīng)發(fā)展為各種不同的格式以利消費(fèi)者將視頻記錄下來(lái)。網(wǎng)絡(luò)技術(shù)的發(fā)達(dá)也促使視頻的紀(jì)錄片段以串流媒體的形式存在于因特網(wǎng)之上并可被電腦接收與播放。視頻與電影屬于不同的技術(shù),后者是利用照相術(shù)將動(dòng)態(tài)的影像捕捉為一系列的靜態(tài)照片。
幀率:幀率(Frame rate)是以幀稱(chēng)為單位的位圖圖像連續(xù)出現(xiàn)在顯示器上的頻率(速率)。
幀率和人類(lèi)視覺(jué):人類(lèi)視覺(jué)的時(shí)間敏感度和分辨率根據(jù)視覺(jué)刺激的類(lèi)型和特征而變化,并且在個(gè)體之間不同。人類(lèi)視覺(jué)系統(tǒng)每秒可處理10到12個(gè)圖像并單獨(dú)感知它們,而較高的速率則被視為運(yùn)動(dòng)。當(dāng)速率高于50 Hz至90 Hz時(shí),大多數(shù)研究參與者認(rèn)為調(diào)制光(如計(jì)算機(jī)顯示器)穩(wěn)定。這種調(diào)制光的穩(wěn)定感被稱(chēng)為閃爍融合閾值。然而,當(dāng)調(diào)制光是不均勻的并且包含圖像時(shí),閃爍融合閾值可以高得多,數(shù)百赫茲。關(guān)于圖像識(shí)別,已經(jīng)發(fā)現(xiàn)人們?cè)诓婚g斷的一系列不同圖像中識(shí)別特定圖像,每個(gè)圖像持續(xù)少至13毫秒。視力的持久性有時(shí)會(huì)導(dǎo)致非常短的單毫秒視覺(jué)刺激,其感知持續(xù)時(shí)間在100毫秒到400毫秒之間。非常短的多個(gè)刺激有時(shí)被認(rèn)為是單個(gè)刺激,例如10毫秒的綠色閃光,緊接著是10毫秒的紅色閃光,被感知為單個(gè)黃色閃光。
編解碼:為什么要對(duì)視頻編解碼?因?yàn)槠鋵?shí)視頻就是一幀一幀的圖片。一部25幀每秒,90分鐘,分辨率為1024*768,24位真彩色的視頻,沒(méi)有經(jīng)過(guò)壓縮,大小為:
1Byte(字節(jié)) = 8bit(位)
一幀大小 = 1024 * 768 * 24 = 18874368(bit) = 2359296(Byte)
總幀數(shù) = 90 * 60 * 25 = 135000
總大小 = 一幀大小 * 總幀數(shù) = 2359296 * 135000 = 318504960000(Byte)= 303750(MB)≈ 296(GB)
意味著我們存儲(chǔ)一部90分鐘電影需要占用我們296G的存儲(chǔ)空間,所以要對(duì)視頻進(jìn)行壓縮處理,這種技術(shù)就是對(duì)視頻的編碼,相反就是對(duì)視頻的解碼;
解協(xié)議:就是將流媒體協(xié)議的數(shù)據(jù),解析為標(biāo)準(zhǔn)的相應(yīng)的封裝格式數(shù)據(jù)。視音頻在網(wǎng)絡(luò)上傳播的時(shí)候,常常采用各種流媒體協(xié)議,例如HTTP,RTMP,或是MMS等等。這些協(xié)議在傳輸視音頻數(shù)據(jù)的同時(shí),也會(huì)傳輸一些信令數(shù)據(jù)。這些信令數(shù)據(jù)包括對(duì)播放的控制(播放,暫停,停止),或者對(duì)網(wǎng)絡(luò)狀態(tài)的描述等。解協(xié)議的過(guò)程中會(huì)去除掉信令數(shù)據(jù)而只保留視音頻數(shù)據(jù)。例如,采用RTMP協(xié)議傳輸?shù)臄?shù)據(jù),經(jīng)過(guò)解協(xié)議操作后,輸出FLV格式的數(shù)據(jù)。
解封裝:就是將輸入的封裝格式的數(shù)據(jù),分離成為音頻流壓縮編碼數(shù)據(jù)和視頻流壓縮編碼數(shù)據(jù)。封裝格式種類(lèi)很多,例如MP4,MKV,RMVB,TS,F(xiàn)LV,AVI等等,它的作用就是將已經(jīng)壓縮編碼的視頻數(shù)據(jù)和音頻數(shù)據(jù)按照一定的格式放到一起。例如,F(xiàn)LV格式的數(shù)據(jù),經(jīng)過(guò)解封裝操作后,輸出H.264編碼的視頻碼流和AAC編碼的音頻碼流。
解碼:就是將視頻/音頻壓縮編碼數(shù)據(jù),解碼成為非壓縮的視頻/音頻原始數(shù)據(jù)。音頻的壓縮編碼標(biāo)準(zhǔn)包含AAC,MP3,AC-3等等,視頻的壓縮編碼標(biāo)準(zhǔn)則包含H.264,MPEG2,VC-1等等。解碼是整個(gè)系統(tǒng)中最重要也是最復(fù)雜的一個(gè)環(huán)節(jié)。通過(guò)解碼,壓縮編碼的視頻數(shù)據(jù)輸出成為非壓縮的顏色數(shù)據(jù),例如YUV420P,RGB等等;壓縮編碼的音頻數(shù)據(jù)輸出成為非壓縮的音頻抽樣數(shù)據(jù),例如PCM數(shù)據(jù)。
音視頻同步:就是根據(jù)解封裝模塊處理過(guò)程中獲取到的參數(shù)信息,同步解碼出來(lái)的視頻和音頻數(shù)據(jù),并將視頻音頻數(shù)據(jù)送至系統(tǒng)的顯卡和聲卡播放出來(lái)。
下面開(kāi)始實(shí)戰(zhàn),使用ffmpeg實(shí)現(xiàn)視頻播放;
視頻存儲(chǔ)目錄:設(shè)備跟目錄下的/demo.mp4文件
繼續(xù)上篇所講,我們項(xiàng)目配置好了ffmpeg之后能夠獲取到ffmpeg版本號(hào)說(shuō)明配置沒(méi)有問(wèn)題;
實(shí)現(xiàn)原理簡(jiǎn)述:
視頻顯示層:Java層使用SurfaceView顯示,本地層(C/C++)使用ANativeWindow來(lái)實(shí)現(xiàn)渲染
視頻編解碼:使用FFmpeg來(lái)進(jìn)行解碼,而不是基于Android自帶的MediaPlayer播放器,也不基于Android的mediacodec硬件解碼;
視頻解碼流程:

接著開(kāi)始編寫(xiě)Java代碼:
1、視頻播放界面布局文件:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
? ? tools:context=".MainActivity">
? ? <SurfaceView
? ? ? ? android:id="@+id/surfaceView"
? ? ? ? android:layout_width="match_parent"
? ? ? ? android:layout_height="200dp" />
? ? <SeekBar
? ? ? ? android:id="@+id/seekBar"
? ? ? ? android:layout_below="@id/surfaceView"
? ? ? ? android:layout_width="match_parent"
? ? ? ? android:layout_height="wrap_content"
? ? ? ? android:max="100"
? ? ? ? android:visibility="gone" />
? ? <Button
? ? ? ? android:layout_width="match_parent"
? ? ? ? android:layout_height="50dp"
? ? ? ? android:gravity="center"
? ? ? ? android:onClick="play"
? ? ? ? android:layout_below="@id/seekBar"
? ? ? ? android:text="Play" />
</RelativeLayout>
2、編寫(xiě)播放視頻控制類(lèi):MyPlayer.java
package com.yan.myplayer2;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MyPlayer implements SurfaceHolder.Callback {
? ? static {
? ? ? ? System.loadLibrary("myPlayer");
? ? }
? ? private SurfaceHolder surfaceHolder;
? ? public void setSurfaceView(SurfaceView surfaceView) {
? ? ? ? if (null != this.surfaceHolder) {
? ? ? ? ? ? this.surfaceHolder.removeCallback(this);
? ? ? ? }
? ? ? ? this.surfaceHolder = surfaceView.getHolder();
? ? ? ? this.surfaceHolder.addCallback(this);
? ? }
? ? @Override
? ? public void surfaceCreated(SurfaceHolder surfaceHolder) {
? ? }
? ? @Override
? ? public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
? ? ? ? this.surfaceHolder = surfaceHolder;
? ? }
? ? @Override
? ? public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
? ? }
? ? public void start(String absolutePath) {
? ? ? ? native_start(absolutePath, surfaceHolder.getSurface());
? ? }
? ? public native void native_start(String absolutePath, Surface surface);
}
3、C/C++層實(shí)現(xiàn)代碼
package com.yan.myplayer2;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MyPlayer implements SurfaceHolder.Callback {
? ? static {
? ? ? ? System.loadLibrary("myPlayer");
? ? }
? ? private SurfaceHolder surfaceHolder;
? ? public void setSurfaceView(SurfaceView surfaceView) {
? ? ? ? if (null != this.surfaceHolder) {
? ? ? ? ? ? this.surfaceHolder.removeCallback(this);
? ? ? ? }
? ? ? ? this.surfaceHolder = surfaceView.getHolder();
? ? ? ? this.surfaceHolder.addCallback(this);
? ? }
? ? @Override
? ? public void surfaceCreated(SurfaceHolder surfaceHolder) {
? ? }
? ? @Override
? ? public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
? ? ? ? this.surfaceHolder = surfaceHolder;
? ? }
? ? @Override
? ? public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
? ? }
? ? public void start(String absolutePath) {
? ? ? ? native_start(absolutePath, surfaceHolder.getSurface());
? ? }
? ? public native void native_start(String absolutePath, Surface surface);
}
4、CmakeLists.xml文件修改,編譯時(shí)需要另外導(dǎo)入一些底層庫(kù)文件
\app\CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
add_library(
? ? ? ? myPlayer
? ? ? ? SHARED
? ? ? ? src/main/cpp/native-lib.cpp)
include_directories(src/main/cpp/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}")
find_library(
? ? ? ? log-lib
? ? ? ? log)
target_link_libraries(
? ? ? ? myPlayer
? ? ? ? avfilter avformat avcodec avutil swresample swscale
? ? ? ? ${log-lib}
? ? ? ? android? #ANativeWindow 需要導(dǎo)入android庫(kù),不然編譯失敗
? ? ? ? z? ? ? ? # 編譯時(shí)需要一些底層庫(kù)文件導(dǎo)入
? ? ? ? OpenSLES #
)
5、C/C++層代碼實(shí)現(xiàn):\app\src\main\cpp\native-lib.cpp
#include <jni.h>
#include <string>
#include <android/native_window_jni.h>
#include <unistd.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/frame.h>
}
extern "C" {
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_yan_myplayer2_MainActivity_stringFromJNI(
? ? ? ? JNIEnv *env,
? ? ? ? jobject /* this */) {
? ? std::string hello = "Hello from C++";
? ? return env->NewStringUTF(av_version_info());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_yan_myplayer2_MyPlayer_native_1start(JNIEnv *env, jobject instance, jstring absolutePath_,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? jobject surface) {
? ? //獲取渲染窗口
? ? ANativeWindow *aNativeWindow = ANativeWindow_fromSurface(env, surface);
? ? // TODO? FFmpeg 視頻繪制
? ? const char *absolutePath = env->GetStringUTFChars(absolutePath_, 0);
? ? // 1、初始化網(wǎng)絡(luò)模塊
? ? avformat_network_init();
? ? // 2、獲取視頻流
? ? AVFormatContext *avFormatContext = avformat_alloc_context();
? ? AVDictionary *opts = NULL;
? ? av_dict_set(&opts, "timeout", "3000000", 0);
? ? int ret = avformat_open_input(&avFormatContext, absolutePath, NULL, &opts);
? ? if (ret) {
? ? ? ? return;
? ? }
? ? // 視頻流索引
? ? int video_stream_index = -1;
? ? // 獲取視頻信息
? ? avformat_find_stream_info(avFormatContext, NULL);
? ? for (int i = 0; i < avFormatContext->nb_streams; ++i) {
? ? ? ? // 判斷如果是視頻類(lèi)型就賦值
? ? ? ? if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
? ? ? ? ? ? video_stream_index = i;
? ? ? ? ? ? break;
? ? ? ? }
? ? }
? ? // 獲取解碼器參數(shù)
? ? AVCodecParameters *avCodecParameters = avFormatContext->streams[video_stream_index]->codecpar;
? ? //獲取解碼器
? ? AVCodec *avCodec = avcodec_find_decoder(avCodecParameters->codec_id);
? ? //解碼器上下文
? ? AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
? ? // 將解碼器參數(shù)copy到解碼器上下文
? ? avcodec_parameters_to_context(avCodecContext, avCodecParameters);
? ? //打開(kāi)解碼器
? ? avcodec_open2(avCodecContext, avCodec, NULL);
? ? // 將YUV的數(shù)據(jù)轉(zhuǎn)化為RGB數(shù)據(jù)
? ? SwsContext *swsContext = sws_getContext(avCodecContext->width, avCodecContext->height,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? avCodecContext->pix_fmt,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? avCodecContext->width, avCodecContext->height,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? AV_PIX_FMT_RGBA, SWS_BILINEAR, 0,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0, 0);
? ? // 設(shè)置ANativeWindow窗口緩存區(qū)
? ? ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width, avCodecContext->height,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? WINDOW_FORMAT_RGBA_8888);
? ? ANativeWindow_Buffer outBuffer;
? ? // 解碼 yuv數(shù)據(jù)
? ? AVPacket *avPacket = av_packet_alloc();
? ? //從視頻數(shù)據(jù)中獲取數(shù)據(jù)包
? ? while (av_read_frame(avFormatContext, avPacket) >= 0) {
? ? ? ? avcodec_send_packet(avCodecContext, avPacket);
? ? ? ? AVFrame *avFrame = av_frame_alloc();
? ? ? ? int ret = avcodec_receive_frame(avCodecContext, avFrame);
? ? ? ? if (ret == AVERROR(EAGAIN)) {
? ? ? ? ? ? continue;
? ? ? ? } else if (ret < 0) {
? ? ? ? ? ? break;
? ? ? ? }
? ? ? ? // 接受數(shù)據(jù)的容器
? ? ? ? uint8_t *dst_data[4];
? ? ? ? // 每一幀每一行的首地址
? ? ? ? int dst_linesize[4];
? ? ? ? av_image_alloc(dst_data, dst_linesize, avCodecContext->width, avCodecContext->height,
? ? ? ? ? ? ? ? ? ? ? AV_PIX_FMT_RGBA, 1);
? ? ? ? // 繪制
? ? ? ? sws_scale(swsContext, avFrame->data, avFrame->linesize, 0, avFrame->height, dst_data,
? ? ? ? ? ? ? ? ? dst_linesize);
? ? ? ? // 開(kāi)始渲染
? ? ? ? // 渲染前鎖住窗口,渲染結(jié)束解鎖窗口
? ? ? ? ANativeWindow_lock(aNativeWindow, &outBuffer, NULL);
? ? ? ? //渲染
? ? ? ? uint8_t *firstWindow = static_cast<uint8_t *>(outBuffer.bits);
? ? ? ? // 輸入源(RGB)
? ? ? ? uint8_t *src_data = dst_data[0];
? ? ? ? //一行有多少個(gè)字節(jié) RGBA
? ? ? ? int dstStride = outBuffer.stride * 4;
? ? ? ? int src_linesize = dst_linesize[0];
? ? ? ? for (int i = 0; i < outBuffer.height; ++i) {
? ? ? ? ? ? // 通過(guò)內(nèi)存拷貝來(lái)進(jìn)行渲染
? ? ? ? ? ? memcpy(firstWindow + i * dstStride, src_data + i * src_linesize, dstStride);
? ? ? ? }
? ? ? ? ANativeWindow_unlockAndPost(aNativeWindow);
? ? ? ? usleep(1000 * 16);
? ? ? ? av_frame_free(&avFrame);
? ? }
? ? env->ReleaseStringUTFChars(absolutePath_, absolutePath);
}
6、\app\src\main\AndroidManifest.xml,權(quán)限添加:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
7、初始化MyPlayer,并實(shí)現(xiàn)播放:
package com.yan.myplayer2;
import android.Manifest;
import android.app.AlertDialog;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
public class MainActivity extends AppCompatActivity {
? ? private static final int CODE_FOR_WRITE_PERMISSION = 1001;
? ? private SurfaceView surfaceView;
? ? private MyPlayer myPlayer;
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
? ? ? ? ? ? ? ? WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
? ? ? ? setContentView(R.layout.activity_main);
? ? ? ? surfaceView = findViewById(R.id.surfaceView);
? ? ? ? myPlayer = new MyPlayer();
? ? ? ? myPlayer.setSurfaceView(surfaceView);
? ? ? ? //使用兼容庫(kù)就無(wú)需判斷系統(tǒng)版本
? ? ? ? int hasWriteStoragePermission = ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
? ? ? ? if (hasWriteStoragePermission == PackageManager.PERMISSION_GRANTED) {
? ? ? ? ? ? //擁有權(quán)限,執(zhí)行操作
? ? ? ? ? ? Toast.makeText(this, "擁有權(quán)限", Toast.LENGTH_SHORT).show();
? ? ? ? } else {
? ? ? ? ? ? //沒(méi)有權(quán)限,向用戶(hù)請(qǐng)求權(quán)限
? ? ? ? ? ? ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_FOR_WRITE_PERMISSION);
? ? ? ? }
? ? }
? ? public void play(View view) {
? ? ? ? File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "demo.mp4");
? ? ? ? if (file.exists()) {
? ? ? ? ? ? myPlayer.start(file.getAbsolutePath());
? ? ? ? } else {
? ? ? ? ? ? Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
? ? ? ? }
? ? }
? ? @Override
? ? public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
? ? ? ? //通過(guò)requestCode來(lái)識(shí)別是否同一個(gè)請(qǐng)求
? ? ? ? if (requestCode == CODE_FOR_WRITE_PERMISSION) {
? ? ? ? ? ? if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
? ? ? ? ? ? ? ? //用戶(hù)同意,執(zhí)行操作
? ? ? ? ? ? ? ? //initScan();
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? //用戶(hù)不同意,向用戶(hù)展示該權(quán)限作用
? ? ? ? ? ? ? ? if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
? ? ? ? ? ? ? ? ? ? new AlertDialog.Builder(MainActivity.this)
? ? ? ? ? ? ? ? ? ? ? ? ? ? .setMessage(R.string.storage_permissions_remind)
? ? ? ? ? ? ? ? ? ? ? ? ? ? .setPositiveButton("OK", (dialog1, which) ->
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ActivityCompat.requestPermissions(MainActivity.this,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? CODE_FOR_WRITE_PERMISSION))
? ? ? ? ? ? ? ? ? ? ? ? ? ? .setNegativeButton("Cancel", null)
? ? ? ? ? ? ? ? ? ? ? ? ? ? .create()
? ? ? ? ? ? ? ? ? ? ? ? ? ? .show();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
8、運(yùn)行app播放視頻;
