AndroidStudio集成FFmpeg,实现音视频同步--FFmpeg实现视频播放

音视频播放流程图:


视频播放原理解析

视频:视频(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播放视频;


源码分享:https://github.com/YWeiY/MyPlayer

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,387评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,845评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,091评论 0 246
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,308评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,662评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,795评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,008评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,466评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,687评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,181评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,531评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,177评论 3 239
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,126评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,862评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,734评论 2 274

推荐阅读更多精彩内容