音视频播放流程图:
视频播放原理解析:
视频:视频(Video)泛指将一系列静态影像以电信号的方式加以捕捉、纪录、处理、储存、传送与重现的各种技术。连续的图像变化每秒超过24帧(frame)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面;看上去是平滑连续的视觉效果,这样连续的画面叫做视频。视频技术最早是为了电视系统而发展,但现在已经发展为各种不同的格式以利消费者将视频记录下来。网络技术的发达也促使视频的纪录片段以串流媒体的形式存在于因特网之上并可被电脑接收与播放。视频与电影属于不同的技术,后者是利用照相术将动态的影像捕捉为一系列的静态照片。
帧率:帧率(Frame rate)是以帧称为单位的位图图像连续出现在显示器上的频率(速率)。
帧率和人类视觉:人类视觉的时间敏感度和分辨率根据视觉刺激的类型和特征而变化,并且在个体之间不同。人类视觉系统每秒可处理10到12个图像并单独感知它们,而较高的速率则被视为运动。当速率高于50 Hz至90 Hz时,大多数研究参与者认为调制光(如计算机显示器)稳定。这种调制光的稳定感被称为闪烁融合阈值。然而,当调制光是不均匀的并且包含图像时,闪烁融合阈值可以高得多,数百赫兹。关于图像识别,已经发现人们在不间断的一系列不同图像中识别特定图像,每个图像持续少至13毫秒。视力的持久性有时会导致非常短的单毫秒视觉刺激,其感知持续时间在100毫秒到400毫秒之间。非常短的多个刺激有时被认为是单个刺激,例如10毫秒的绿色闪光,紧接着是10毫秒的红色闪光,被感知为单个黄色闪光。
编解码:为什么要对视频编解码?因为其实视频就是一帧一帧的图片。一部25帧每秒,90分钟,分辨率为1024*768,24位真彩色的视频,没有经过压缩,大小为:
1Byte(字节) = 8bit(位)
一帧大小 = 1024 * 768 * 24 = 18874368(bit) = 2359296(Byte)
总帧数 = 90 * 60 * 25 = 135000
总大小 = 一帧大小 * 总帧数 = 2359296 * 135000 = 318504960000(Byte)= 303750(MB)≈ 296(GB)
意味着我们存储一部90分钟电影需要占用我们296G的存储空间,所以要对视频进行压缩处理,这种技术就是对视频的编码,相反就是对视频的解码;
解协议:就是将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。
解封装:就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。
解码:就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。
音视频同步:就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。
下面开始实战,使用ffmpeg实现视频播放;
视频存储目录:设备跟目录下的/demo.mp4文件
继续上篇所讲,我们项目配置好了ffmpeg之后能够获取到ffmpeg版本号说明配置没有问题;
实现原理简述:
视频显示层:Java层使用SurfaceView显示,本地层(C/C++)使用ANativeWindow来实现渲染
视频编解码:使用FFmpeg来进行解码,而不是基于Android自带的MediaPlayer播放器,也不基于Android的mediacodec硬件解码;
视频解码流程:
接着开始编写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、编写播放视频控制类: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++层实现代码
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文件修改,编译时需要另外导入一些底层库文件
\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 需要导入android库,不然编译失败
z # 编译时需要一些底层库文件导入
OpenSLES #
)
5、C/C++层代码实现:\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、初始化网络模块
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) {
// 判断如果是视频类型就赋值
if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
break;
}
}
// 获取解码器参数
AVCodecParameters *avCodecParameters = avFormatContext->streams[video_stream_index]->codecpar;
//获取解码器
AVCodec *avCodec = avcodec_find_decoder(avCodecParameters->codec_id);
//解码器上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
// 将解码器参数copy到解码器上下文
avcodec_parameters_to_context(avCodecContext, avCodecParameters);
//打开解码器
avcodec_open2(avCodecContext, avCodec, NULL);
// 将YUV的数据转化为RGB数据
SwsContext *swsContext = sws_getContext(avCodecContext->width, avCodecContext->height,
avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height,
AV_PIX_FMT_RGBA, SWS_BILINEAR, 0,
0, 0);
// 设置ANativeWindow窗口缓存区
ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width, avCodecContext->height,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer outBuffer;
// 解码 yuv数据
AVPacket *avPacket = av_packet_alloc();
//从视频数据中获取数据包
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;
}
// 接受数据的容器
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);
// 开始渲染
// 渲染前锁住窗口,渲染结束解锁窗口
ANativeWindow_lock(aNativeWindow, &outBuffer, NULL);
//渲染
uint8_t *firstWindow = static_cast<uint8_t *>(outBuffer.bits);
// 输入源(RGB)
uint8_t *src_data = dst_data[0];
//一行有多少个字节 RGBA
int dstStride = outBuffer.stride * 4;
int src_linesize = dst_linesize[0];
for (int i = 0; i < outBuffer.height; ++i) {
// 通过内存拷贝来进行渲染
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,权限添加:
<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,并实现播放:
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);
//使用兼容库就无需判断系统版本
int hasWriteStoragePermission = ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (hasWriteStoragePermission == PackageManager.PERMISSION_GRANTED) {
//拥有权限,执行操作
Toast.makeText(this, "拥有权限", Toast.LENGTH_SHORT).show();
} else {
//没有权限,向用户请求权限
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) {
//通过requestCode来识别是否同一个请求
if (requestCode == CODE_FOR_WRITE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//用户同意,执行操作
//initScan();
} else {
//用户不同意,向用户展示该权限作用
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、运行app播放视频;