王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

为什么要学习音视频

核心竞争力,高端人才相当缺乏,技术迭代慢,

为什么音视频学不好

资料比较少,音视频最难的地方在于编码,没有形成完整的体系

关于音视频编码

1,视频文件:MP4,RMVB, AVI,FLV
2,现在学音视频和以前的区别,
以前:播放本地文件,
现在:播放网络流(视频流和音频流)
3,RMVB、MP4等是封装格式,是一个容器,包含音频流和视频流
在这里插入图片描述
4,在网络上传播不传RMVB、MP4这些封装格式,我们传播音频流和视频流。
5,编码的本质是压缩,H264就是一种视频编码格式 ,压缩方式不一样,就生成了各种视频格式。其它视频格式如下
在这里插入图片描述
音频格式如下
在这里插入图片描述

6,原始视频数据从摄像头采集来,叫yuv.原始音频数据从麦克风采集叫做pcm。
把音频流视频流封装到同一个文件,组合方式不一样,就有了不同的封装格式
在这里插入图片描述
7,两个机构ITU-T 和 ISO
在这里插入图片描述

ITU-T 研发:H261 H262 H263
ISO 研发:Mpeg1 Mpeg2
共同研发;H264/Mepg4-Avc,H265/HEVC.前者是ITU-T起的名字,后者是ISO起的名字
android 支持H265
google研发:VP8 VP9,主要应用在视频通话
音视频编码的鼻祖:H621(块结构编码)
8,MediaCodec,Android中的编解码
9,视频编码历史
在这里插入图片描述

10,为什么H261这么厉害,因为它采用了块结构的混合编码
现在有一帧图片,长200,宽100
总计像素有200X100个,如果不压缩保存到文件中,需要2万个像素X4个字节,一个像素四个字节,需要8万个字节保存这张图片。现在我们得知这张图片是个渐变的。我们可以这样存储图片。
我们先保存宽高200,100(需要两个int保存),存储起始点和终始点各需要两个int。存储起始点颜色和终始点颜色需要2个int。这样我们就不需要8万个像素了。
我们无线放大的时候会发现图片在某个范围都可以看成是渐变的。
视频编码肯定是有损的。
电影院的视频是无损的。两个小时视频需要几千G
11,H264格式图片压缩
在这里插入图片描述
在这里插入图片描述
12,使用ffmpeg

      
提取音频  
ffmpeg -i input.mp4 -acodec copy -vn  output.aac
  
提取视频
ffmpeg -i input.mp4 -c:v copy -bsf:v h264_mp4toannexb -an out.h264

播放视频yuv
ffplay -f rawvideo -video_size 368x384 codec.h265
   

  
ffmpeg -i input.mp4 -codec:a  pcm_f32le -ar 44100 -ac 2 -f f32le output.pcm
 
播放直播
ffplay -i  rtmp://58.200.131.2:1935/livetv/cctv1
播放H265视频  
 ffplay -stats -f hevc  codec.h265
播放H264视频
ffplay -stats -f h264 codec.h264
       

  
ffmpeg -i input.mp4 -f mp3 -vn apple.mp3

ffplay -ar 48000 -channels 2 -f f32le -i output.pcm
1.视频倒放,无音频
ffmpeg.exe -i input.mp4 -filter_complex [0:v]reverse[v] -map [v] -preset superfast reversed.mp4
 
2.视频倒放,音频不变
ffmpeg.exe -i input.mp4 -vf reverse reversed.mp4
   
3.音频倒放,视频不变
ffmpeg.exe -i input.mp4 -map 0 -c:v copy -af "areverse" reversed_audio.mp4
 
4.音视频同时倒放
ffmpeg.exe -i input.mp4 -vf reverse -af areverse -preset superfast reversed.mp4
  
PDF转 Word
https://app.xunjiepdf.com/pdf2word/
视频裁剪
ffmpeg  -i ./input.mp4 -vcodec copy -acodec copy -ss 00:00:00 -to 00:05:00 ./cutout1.mp4 -y
ffmpeg  -i ./input.mp4 -vcodec copy -acodec copy -ss 00:05:00 -to 00:10:00 ./cutout2.mp4 -y
ffmpeg  -i ./input.mp4 -vcodec copy -acodec copy -ss 00:10:00 -to 00:14:50./cutout3.mp4 -y
opengl+rtmp

12,什么是H264
在这里插入图片描述

13,视频编码为什么用yuv而不用RGB
在这里插入图片描述
yuv没有uv就是黑白电视
在这里插入图片描述
4个y配一个v,一个u,但是宏观上(也就是人眼看到的)和左边的是一样的效果
rgb需要三个通道,yuv只需要一个通道
yuv图片宽高取决于y,不取决于u或v
一帧4比1比1的yuv的大小:wh+1/4wh+1/4wh = 3/2wh

14,yuv格式
在这里插入图片描述
在这里插入图片描述

苹果等大部分用的是nv12,Android特殊,用的是nv21。在Android中进行音视频开发需要转换处理。

1,h264编码器:
在这里插入图片描述
CIF/QCIF就是一帧图片的意思
2,压缩是为了减少冗余,包括帧内冗余,帧间冗余。 第一帧走帧内编码,以减少帧内冗余为主。第二帧基本上都走帧间编码,减少帧间冗余。
3,帧内冗余处理
视频信源编码器:划分N个宏块,对每个宏块进行预测方向
复合编码器:整理残差(剩下的左边和上边的数据)数据和预测方向
传输缓冲器:检验产品是否合格
传出编码器:产品摆放
4,帧间冗余处理
第一帧和第二帧相差不大,比如第一帧的一个汽车已经编码了,第二帧就不需要再次编码。假如第二帧的汽车位置相对于第一帧发生了变化,我们就可以记录该宏块的位移矢量,而不需要再次编码。
所以会首先会判断宏块在前面是否编码了。如果已经编码,直接使用运动矢量就可以了。
第一帧叫做I帧,使用运动矢量的叫p帧。

5,GOP:图像序列,可以理解成一个场景,场景的物体都是相似的。两个I帧之内可以认为是一个GOP
Gop影响着seek,性能优化等
6,I帧最大,P帧(运动矢量)比I帧小,B帧是计算出来的最小
7,直播重视秒开率。I帧会比较多。
8,输出的时候,第一帧I帧最先输出,第二帧B帧会缓存到传输缓冲器,第三帧B帧依然会被缓存到传输缓冲器,第四帧是p帧,输出。然后所有缓存的B帧输出。
输出的帧顺序与播放帧的顺序不一样。播放的顺序是按照pts。比如你的帧间隔是10毫秒,I帧为0毫秒,p帧为40毫秒,两个B帧分别为20,30毫秒。播放器拿到I帧会直接显示,但是拿到p帧后,因为和上一帧I帧间隔了40毫秒(大于10毫秒),所以不会输出,会缓存起来,等显示30毫秒的B帧后才会显示40毫秒的p帧。
在这里插入图片描述

可以看出,B帧输出是没有顺序的。
9,编码I帧最简单,编码B帧耗时最长,
10,h262解码:算法高度相同,相当复杂,
11,如何保证帧的完整性?
使用分隔符,0x0000001或者0x00001。
如果像素刚好是0x000001呢?把0x000001变成0x000301,
如果像素刚好是0x000301呢?直接变成0x000001。改变一个像素不影响用户体验
12,根据分隔符可以算出I帧,p帧,b帧的大小。
13, 第一个叫数字做帧类型(如下图)。67代表sps,68代表pps,65代表I帧,41代表p帧,01代表b帧
在这里插入图片描述
I帧之后是p帧
在这里插入图片描述

下图蓝色阴影为b帧
在这里插入图片描述

14,cpu解码:软解码,兼容性高,因为每个手机都有cpu
元器件解码:硬解码,元器件封装到一个硬件中叫DSP,兼容性差,因为手机厂商用的硬件未必相同
硬解码优点:不卡顿,耗电量低,可以解析多路视频(监控)
在这里插入图片描述在这里插入图片描述
GPU不做解码,只做解码后的显示
15,dsp:硬件手机厂商不同,dsp也不同。有兼容性问题。解决办法:先硬解,硬解不支持在走软解
16,MediaPlayer:硬解,支持的播放格式较少。
dsp可以访问磁盘(cpu能访问,dsp就能访问),直接读取sdcard数据,dsp解码16进制数据,最终形成yuv,yuv数据交给GPU渲染。假如视频有两个小时长度,那么cpu执行完代码后就不再参与,读第二帧后cpu就不再参与,直到读到文件结尾。
在这里插入图片描述
右侧一堆数字是yuv数据。
17,java代码不能直接读dsp,需要使用Mediacodec。
18,MediaCodec是硬解码,就是为了调用dsp
19,MediaCodec基于过程,很难学

下面自己写代码解析H264。(对应第三节课)

就是把下图这些东东还原成视频
在这里插入图片描述
MediaCodec就是为了调用dsp,虽然是Java代码写的,MediaCodec却是基于过程的。
在cpu中读出文件,从cpu把数据传给dsp,这是跨设备(不在同一个物理设备)。因此MediaCodec(横跨cpu和dsp)没有设置回调方法,解码成功后无法从回调方法拿到解码成功后的数据。
因此MediaCode采取了另一种方式,dsp提供一个数量为8的队列,每个容器都有多个状态,容器有数据后,放到Codec解码。数据完成从cpu流动到dsp,解码后的ypu在流到cpu
在这里插入图片描述

布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

添加权限

 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
MainActivity文件
package com.example.audiovideotest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.content.pm.PackageManager;
import android.media.MediaCodec;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.io.File;

public class MainActivity extends AppCompatActivity {
    private H264Player h264Player;
    //动态获取读写权限
    public boolean checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
            }, 1);

        }
        return false;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       checkPermission();
       initSurface();
    }
   //SurfaceView画框,Surface画布,surface绘制,surfaceview显示 
    private void initSurface() {
        SurfaceView surfaceView = findViewById(R.id.preview);
        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
            //拿到surface,绘制、渲染是在surface上
                Surface surface = surfaceHolder.getSurface();
                h264Player = new H264Player(new File(Environment.getExternalStorageDirectory(),"out.h264").getAbsolutePath(),surface);
                h264Player.play();
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {

            }

            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {

            }
        });
    }
}
package com.example.audiovideotest;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

//解码是耗时的,继承线程
public class H264Player implements Runnable {
    //数据源
    private String path;
    //解码器,把数据源解码成yuv,显示在surface上
    private MediaCodec mediaCodec;
    //显示目的地,就是surface
    private Surface surface;

    public H264Player(String path, Surface surface) {
        this.path = path;
        this.surface = surface;
        try {
            //1,创建解码器,解码器不区分音频视频解码器,如果创建
            //视频解码器就以video开头,如果创建音频解码器就以audio开头。
            //2,video后面接的是具体的编码格式,如h264,h265,vp8,vp9
            //3,avc就是mpeg4-avc就是h264
            // 4,MediaFormat.MIMETYPE_VIDEO_AVC就是"video/avc"
            //5,硬解码只支持几种编码格式,因为dsp硬件有限,不可能兼容所有格式(比如RV40 )
            // mediaCodec  = MediaCodec.createDecoderByType("video/avc");
            mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
            //赋值自己的参数,告诉dsp的信息.
            //MediaFormat 封装了HashMap,与HashMap不同的是,MediaFormat 的key是死的。
            //构建自己的MediaFormat ,视频流是"video/avc",宽高自己随便写
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 200, 200);
            //帧率
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            //surface如果不配置,就不渲染,第三个参数是加密,第四个参数是标志位,解码是0就可以了
            mediaCodec.configure(mediaFormat, surface, null, 0);
            Log.i("zhang_xin", "支持");
        } catch (IOException e) {
            e.printStackTrace();
            //如果不支持硬解这里会报异常
            Log.i("zhang_xin", e.getMessage());
        }
    }

    //一旦调用player,MediaCodec就开始工作了。
    public void play() {
        //开始解码
        mediaCodec.start();
        new Thread(this).start();
    }

    @Override
    public void run() {
        //cpu把数据传给dsp是跨设备,无法使用回调。
        //dsp会提供一个数量为8的容器,
        try {
            decodeH264();
        } catch (Exception e) {
            Log.i("david", "run: "+e.toString());
        }
    }

    private void decodeH264() {
        byte[] bytes = null;
        try {
        //数据来到了bytes数组
            bytes = getBytes(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        /** 过时写法
         //拿到所有的容器,不建议这么写,已经过时
         ByteBuffer[] byteBuffers = mediaCodec.getInputBuffers();
         //查询哪个容器可以用,inIndex小于0当前没有容器可以使用。
         //10000为等待时间。告诉dsp我要等10毫秒,单位是微秒
         int inIndex = mediaCodec.dequeueInputBuffer(10000);
         if(inIndex>=0){
         //拿到了容器的号码,根据号码找到了相应的容器
         ByteBuffer byteBuffer = byteBuffers[inIndex];
         }
         */
       
        int startIndex = 0;
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        while (true) {//一直往容器里丢东西,当文件结束的时候停止
            //startIndex必须加大于2的数字,
            int nextFrame = findByFrame(bytes, startIndex+4, bytes.length);
              //上面的方法弃用(标注过时写法的地方)了,因为google认为,我们不应该拿到所有的容器,
             // 在处理完毕之后,一定要调用queueInputBuffer把这个ByteBuffer放回到队列中,这样才能正确释放缓存区。
            int inIndex = mediaCodec.dequeueInputBuffer(10000);
            if (inIndex >= 0) {
                //拿到可用的容器
                ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inIndex);
                //每次往容器里丢一帧数据,注意帧的大小不固定。使用分隔符(00 00 01)来区别帧。注意不能丢全部数据,也不按照固定大小丢。
                //startIndex从0开始
                int length = nextFrame - startIndex;
                //丢byte数组,内容从startIndex开始,丢length个长度
                byteBuffer.put(bytes, startIndex, length);
                //把容器的编号丢给dsp,数据就从cpu流到了dsp,inIndex,就是容器索引
                //第二个参数是偏移,没有偏移
                //第四个参数是pts(时间戳),解码的时候按照视频中的pts解码,编码就不能为0
                //第五个参数flag是0 就可以
                mediaCodec.queueInputBuffer(inIndex,0,length,0,0);
                startIndex = nextFrame;
            }
            //判断解码是否完成。输入与输出不是同步的。传入数据,并不一定马上输出数据,因为解码是耗时的
            //如果索引大于0,就说明解码成功
            //info:假如我传进去是8K,解码完成肯定大于8k,通过info得到解码后的大小。info就是出参入参对象
            //第二个参数:The timeout in microseconds, a negative timeout indicates "infinite".
            int outIndex = mediaCodec.dequeueOutputBuffer(info,10000);
             //大于等于0解码完成,然后渲染出去
            if(outIndex>=0){
                try{
                //解决播放过快的问题
                //播放的视频帧数每秒30帧,每帧播放事件大概是33毫秒,
                Thread.sleep(33);
                }catch(Exception e){
                }
                //配置了surface,这里就是true,直接把数据渲染到配置了的surface
                //渲染到Surface,MediaCodec帮我们完成了
                mediaCodec.releaseOutputBuffer(outIndex,true);
            }
        }
    }
 //返回下一个分割符的位置
 //start:上一次分隔符的开始,start必须要大于起始位置,不然会返回起始位置,我们传人start参数的时候让它加了4
    private int findByFrame(byte[] bytes, int start, int totalSize) {
    //为什么减4,我们在i的基础上往后判断,避免越界
        for (int i = start; i <= totalSize - 4; i++) {
        //第0个等于0,第一个等于0,第二个等于0,第三个等于1,注意分隔符有两种
            if (((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x00) && (bytes[i + 3] == 0x01))
                    || ((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x01))) {
                return i;
            }
        }
        return -1;
    }

    //把数据从磁盘读到byte[]
    private byte[] getBytes(String path) throws IOException {
        InputStream is = new DataInputStream(new FileInputStream(new File(path)));
        int len;
        int size = 1024;
        byte[] buf;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        buf = new byte[size];
        while ((len = is.read(buf, 0, size)) != -1)
            bos.write(buf, 0, len);
        buf = bos.toByteArray();
        return buf;
    }
}

如果大家看不懂代码,可以直接把H264Player 类拿去用。

   //startIndex必须加大于2的数字,
            int nextFrame = findByFrame(bytes, startIndex+3, bytes.length);

这里要解释下,如果startIndex是0,
在这里插入图片描述
第一个第二个第三个第四个符合判断,会直接返回index,这时候index为0,所以必须加大于2的数字

另外,如果文件只有一个I帧可能会播放不出来。因为在很多播放器中会存在一个缓冲区。如果没有p帧和B帧,有可能不会解码。有的播放器只有输出p帧和b帧的时候才会输出图片

将画面编码成h264(对应第四节课第一堂课)

数据源有摄像头,录屏,视频文件等。
通过录屏生成h264
不同的数据源如何编码h264。
录屏是动态申请权限,5.0以上才能录屏
首先添加权限

   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.CAMERA"/>

布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始录屏"
        android:onClick="click"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity

package com.example.endecode;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {
    //录屏工具类
    private MediaProjectionManager mediaProjectionManager;
    private  MediaProjection mediaProjection;
    
    public boolean checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.CAMERA
            }, 1);

        }
        return false;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkPermission();
    }

    public void click(View view) {
        //录屏要动态申请权限,MEDIA_PROJECTION_SERVICE已经在Context中定义好了
        mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
        //请求用户是否同意录屏
        Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
        startActivityForResult(captureIntent,1);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(resultCode!=RESULT_OK||requestCode!=1){return;}
        //通过录屏生成h264
        //通过mediaProjection 实现录屏
        mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
        H264EnCoude h264EnCoude = new H264EnCoude(mediaProjection);
        h264EnCoude.start();
    }
}

MediaProjection 与MediaCodec 是如何工作的呢?
MediaCodec 提供 一个Surface(与解码不同,解码的surface是从SurfaceView中获得的),编码的surface是mediacodec创建的。数据源会利用surface
mediaProjection是输入数据,mediaCodec是编码数据,mediaProjection录到的数据交给mediaCodec。因为都是Google写的,我们不必关系。我们只需要把dsp数据输出给cpu就可以了,输出还要走buffer
视频码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是kbps即千位每秒。通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件。

package com.example.endecode;

import android.hardware.display.DisplayManager;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.projection.MediaProjection;
import android.provider.MediaStore;
import android.view.Surface;

import java.io.IOException;
import java.nio.ByteBuffer;

public class H264EnCoude extends Thread{
    private int width =720;
    private int height = 1280;
    //数据源,既然可以通过录屏获得数据源,我们也可以通过openGL获取数据源,也可以通过射像头
    private MediaProjection mediaProjection;
    //编码器
    private MediaCodec mediaCodec;
    //输出文件,输出h264

    public H264EnCoude(MediaProjection mediaProjection1) {
        this.mediaProjection = mediaProjection1;
        //解码的时候不需要传宽高,但编码必须要有宽高这些基本信息,
        //因为解码会直接去解码h264的配置信息(sps/pps),但编码的时候没有这些配置信息
        MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,width,height);
        try {
           //创建MediaCodec,这里用来编码
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            //帧率,告诉dsp 一秒钟20帧
            format.setInteger(MediaFormat.KEY_FRAME_RATE,20);
            //告诉dsp I帧间隔,30帧一个I帧
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,30);
            //码率,码率越高质量越清晰,一般是width*height
            format.setInteger(MediaFormat.KEY_BIT_RATE,width*height);
            //告诉dsp芯片编码器的数据来源,根据这些信息会生成配置帧 sps pps;我的数据是从Surface中来
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            //surface和加密传null就可以
            //MediaCodec.CONFIGURE_FLAG_ENCODE表示mediaCodec用来是编码的,传0则表示用来解码  的。
            mediaCodec.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
            //创建surface(抢银行例子中的空地),mediaCodec(抢银行例子中的david老师,负责提供场地)。
            Surface inputSurface = mediaCodec.createInputSurface();
            //绑定,录到的数据可以显示到surfaceview,也可以创建虚拟的屏幕
            //name:关系,随便起。不能为Null,保证唯一
            //编码的宽高,最好和上面的宽高相等,一个输出,一个输入
            //3:1个DPI输出3个像素,越大越清晰
            //公开的
            //inputSurface:把从MediaCodec中拿到的inputSurface,提供给mediaProjection
            //回调,什么时候暂停,什么时候恢复,什么时候停止,可以传null
            //使用handler发送消息,这里传null。
            mediaProjection.createVirtualDisplay("jett-davaid",width,height,3, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,inputSurface,null,null);
            //mediaProjection是输入数据,mediaCodec是编码数据,mediaProjection录道的数据交给mediaCodec
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
       super.run();
       //开启编码器
        mediaCodec.start();
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        while (true){
            //输入(mediaProjection录到的数据交给mediaCodec)的地方不需要我们关心,系统帮我们实现了,不需要创建输入的buffer。我们只需要实现输出的代码(从dsp/mediaCodec到cpu),需要创建输出的buffer,
            int outIndex = mediaCodec.dequeueOutputBuffer(info,10000);
            //大于0代表成功
            if(outIndex>0){
                //被编码的数据,需要把容器中(ByteBuffer)的数据放到新建的bute[]中
                ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outIndex);
                byte[] ba = new byte[info.size];
                //把容器byteBuffer里的数据转移到ba数组里
                byteBuffer.get(ba);
                //写道文件中
                FileUtils.writeBytes(ba);//写字节
                //把字节转换为16进制字符串
                FileUtils.writeContent(ba);
                //释放,如果配置了surface,就传true,我们没有配置surface,所以我们传false
                mediaCodec.releaseOutputBuffer(outIndex,false);
            }
        }
    }
}

package com.example.endecode;

import android.os.Environment;
import android.util.Log;

import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;

public class FileUtils {
    private static final String TAG = "David";

    public  static  void writeBytes(byte[] array) {
        FileOutputStream writer = null;
        try {
            // 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
            writer = new FileOutputStream(Environment.getExternalStorageDirectory() + "/codec.h264", true);
            writer.write(array);
            writer.write('\n');


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public  static String writeContent(byte[] array) {
        char[] HEX_CHAR_TABLE = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
        };
        StringBuilder sb = new StringBuilder();
        for (byte b : array) {
            sb.append(HEX_CHAR_TABLE[(b & 0xf0) >> 4]);
            sb.append(HEX_CHAR_TABLE[b & 0x0f]);
        }
        Log.i(TAG, "writeContent: " + sb.toString());
        FileWriter writer = null;
        try {
            // 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
            writer = new FileWriter(Environment.getExternalStorageDirectory() + "/codecH264.txt", true);
            writer.write(sb.toString());
            writer.write("\n");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }


}

OK大功告成。

sps pps

1,sps pps成对出现,是配置帧(基础配置帧和全量配置帧)
2,sps pps帧的内容与MediaCodec配置有关系。配置不同,内容不同
3,MediaCodec 编码器会在第一时间输出sps pps,而且只会输出一个sps pps。
4,视频中会出现多个ssp pps。(联想直播,我进来之前就开播了),第一个ssp pps由MediaCodec 编码器输出,后面的ssp pps由缓存输出,理想情况下是I帧输出一次,ssp pps输出一一次。
5,除了I帧P帧B帧外,还有配置帧
6,如果视频宽高变了或者屏幕旋转了,屏幕会出现黑屏现象,因为sps/pps改变了。这时候需要重新初始化编码器。

数据来源摄像头进行编码(对应第四节课第二堂课)

1,摄像头有camera1,camera2,camerax,我们这里使用camera1
先看下布局文件

<?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"
    android:background="#fff"
    tools:context=".MainActivity">
    <com.maniu.maniumediacodec.LocalSurfaceView
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
</RelativeLayout>
package com.maniu.maniumediacodec;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity1 extends AppCompatActivity {
 
    public boolean checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.CAMERA
            }, 1);

        }
        return false;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main1);
        checkPermission();
    }


}
package com.maniu.maniumediacodec;

import android.content.Context;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import androidx.annotation.NonNull;

import java.io.IOException;

/**
 * camera1输出到SurfaceView
 *
 */
public class LocalSurfaceView  extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback {
    H264Encode h264Encode;
//    mCamera--》surfaceveiw
    private Camera.Size size;
    private Camera mCamera;
//知道预览宽高,可以算出yuv,有多大
    byte[] buffer;
    public LocalSurfaceView(Context context) {
        super(context);
    }

    public LocalSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //设置监听,就会调用onCreate()
        getHolder().addCallback(this);
    }



    public LocalSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    //打开预览
    private void startPreview() {
        //前置摄像头还是后置摄像头
        mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
        //拿到camara 参数
        Camera.Parameters parameters = mCamera.getParameters();
        //得到camera预览大小,预览尺寸。
        size = parameters.getPreviewSize();
        try {
            mCamera.setPreviewDisplay(getHolder());
            //旋转90度,这里旋转的是显示,预览画面并没有因为这个方法的调用
            mCamera.setDisplayOrientation(90);
//            知道预览宽高,可以算出yuv,有多大;width*height+1/4width*height+1/4width*height
            buffer = new byte[size.width * size.height * 3 / 2];
            //camara每次预览的时候把数据放到容器中
            mCamera.addCallbackBuffer(buffer);
            //会回调onPreviewFrame,每帧都会回调。会调用onPreviewFrame();
            mCamera.setPreviewCallbackWithBuffer(this);
            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        //data数组就是yuv,yuv就是图片,如果手机竖着拍照,手机画面拍到的是横着的(因为Android手机摄像头放置是竖着的)
        //旋转,宽高进行交换
        if (h264Encode == null) {
            this.h264Encode = new H264Encode(size.width, size.height);
            h264Encode.startLive();
        }
//        data就是数据,我们这里直接对data进行编码,没有对data进行处理。
        h264Encode.encodeFrame(data);
        //重新设置监听
        mCamera.addCallbackBuffer(data);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        //打开相机预览
        startPreview();
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {

    }
}

package com.maniu.maniumediacodec;

import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;

import java.io.IOException;
import java.nio.ByteBuffer;

public class H264Encode {
    MediaCodec mediaCodec;
    int index;
    int width;
    int height;
    public H264Encode(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public void startLive()  {
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height);
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); //每两秒一个I帧
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);//我们是数据传进来的,yuv420,不再是surface
            mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaCodec.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //把数据从cpu传到dsp,在从dsp传递到cpu;input的大小是固定的,摄像机捕获的是固定的
    //需要对数据进行处理,如果直接保存数据,虽然我们是竖着拍的,但播放会是横着的;需要处理
    //摄像头捕获的数据是nv21,只有Android摄像头支持,但MediaCoc需要接收nv12,需要把nv21转为nv12
    public int encodeFrame(byte[] input) {

        //把数据从cpu传到dsp
        int inputBufferIndex = mediaCodec.dequeueInputBuffer(10000);
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer =   mediaCodec.getInputBuffer(inputBufferIndex);

            inputBuffer.clear();
            inputBuffer.put(input);
            //computPts():pts必须传,解码的时候不需要传,编码的时候必须传
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, computPts(), 0);
            index++;
        }


//        在从dsp传递到cpu
        int outputBufferIndex =   mediaCodec.dequeueOutputBuffer(bufferInfo,100000);
        if (outputBufferIndex >= 0) {
            ByteBuffer  outputBuffer= mediaCodec.getOutputBuffer(outputBufferIndex);
            byte[] data = new byte[bufferInfo.size];
            outputBuffer.get(data);
            FileUtils.writeBytes(data);
            FileUtils.writeContent(data);
            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
        }
        return -1;
    }

//  我们设置的帧率是一秒钟15帧,第一帧的播放时间就是一秒钟/15;第二帧的播放时间就是一秒钟/15*2,依次类推
    //1000000是微妙(视频剪辑都是微妙),换算成秒是1秒,
    public int computPts() {
        return 1000000 / 15 * index;
    }
}

这是我拍摄的视频
在这里插入图片描述
这是我播放的视频
在这里插入图片描述

解决bug

两个地方不对,一个是颜色不对,一个是方向不对。
摄像头推流第一步就是要旋转,另外还需要将nv21(只有Android摄像头支持nv21,非常古老的格式)转换成yuv420(yuv420又名nv12)。

//注17:旋转算法
    public static void portraitData2Raw(byte[] data,byte[] output,int width,int height) {

        int y_len = width * height;

        int uvHeight = height >> 1;
        int k = 0;
        for (int j = 0; j < width; j++) {
            for (int i = height - 1; i >= 0; i--) {
                output[k++] = data[ width * i + j];
            }
        }
        for (int j = 0; j < width; j += 2) {
            for (int i = uvHeight - 1; i >= 0; i--) {
                output[k++] = data[y_len + width * i + j];
                output[k++] = data[y_len + width * i + j + 1];
            }
        }
    }


    // 注13:nv21转变为nv12(又名yuv420)。nv21是yuv的一种子集,只有Android摄像头支持
    // nv21:yyyyyyyyyyyyyyyyyyyyyyy vuvuvuvu VU交叉排列
//    nv12:yyyyyyyyyyyyyyyyyyyy     uvuvuvuv uv交叉排列
    static  byte[] nv12;
    public static byte[]  nv21toNV12(byte[] nv21) {
//        注14:实例化一个容器
        int  size = nv21.length;
        nv12 = new byte[size];
        //注15:Y的范围是0-width*height,y的长度是size*2/3
        int len = size * 2 / 3;
//        注16:把Y拷贝到数组
        System.arraycopy(nv21, 0, nv12, 0, len);
        int i = len;
        while(i < size - 1){
//注17:把偶数的v(0,2,4,6,8等)复制到数组,把奇数的u复制到数组
            nv12[i] = nv21[i + 1];
            nv12[i + 1] = nv21[i];
            i += 2;
        }
        return nv12;
    }

猜你喜欢

转载自blog.csdn.net/qczg_wxg/article/details/125855393