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

这篇具有很好参考价值的文章主要介绍了王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

为什么要学习音视频

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

为什么音视频学不好

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

关于音视频编码

1,视频文件:MP4,RMVB, AVI,FLV
2,现在学音视频和以前的区别,
以前:播放本地文件,
现在:播放网络流(视频流和音频流)
3,RMVB、MP4等是封装格式,是一个容器,包含音频流和视频流
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
4,在网络上传播不传RMVB、MP4这些封装格式,我们传播音频流和视频流。
5,编码的本质是压缩,H264就是一种视频编码格式 ,压缩方式不一样,就生成了各种视频格式。其它视频格式如下
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
音频格式如下
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

6,原始视频数据从摄像头采集来,叫yuv.原始音频数据从麦克风采集叫做pcm。
把音频流视频流封装到同一个文件,组合方式不一样,就有了不同的封装格式
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
7,两个机构ITU-T 和 ISO
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

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,视频编码历史
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

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

13,视频编码为什么用yuv而不用RGB
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
yuv没有uv就是黑白电视
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
4个y配一个v,一个u,但是宏观上(也就是人眼看到的)和左边的是一样的效果
rgb需要三个通道,yuv只需要一个通道
yuv图片宽高取决于y,不取决于u或v
一帧4比1比1的yuv的大小:wh+1/4wh+1/4wh = 3/2wh

14,yuv格式
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

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

1,h264编码器:
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
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帧。
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

可以看出,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帧
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
I帧之后是p帧
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

下图蓝色阴影为b帧
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

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

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

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

布局文件

<?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,
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
第一个第二个第三个第四个符合判断,会直接返回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;
    }
}

这是我拍摄的视频
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)
这是我播放的视频
王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)

解决bug

两个地方不对,一个是颜色不对,一个是方向不对。
摄像头推流第一步就是要旋转,另外还需要将nv21(只有Android摄像头支持nv21,非常古老的格式)转换成yuv420(yuv420又名nv12)。文章来源地址https://www.toymoban.com/news/detail-402315.html

//注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;
    }

到了这里,关于王学岗视频编码————视频编解码基础与MediaCodec编解码(对应1234节)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 什么是视频的编码和解码

    这段描述中,视频解码能力和视频编码能力指的是不同的处理过程。视频解码是将压缩过的视频数据解开并还原为可播放的视频流,而视频编码是将原始视频数据压缩成更小的尺寸,以减少存储空间和传输带宽。在这个上下文中,解码能力和编码能力的区别如下: 视频解码能

    2024年02月13日
    浏览(35)
  • FFMpeg 实现视频解码、编码、转码流程详解

    打开FFmpeg源码,会发现有一系列libavxxx的模块,这些模块很好地划分了代码的结构和分工。 libavformat,format,格式封装 libavcodec,codec,编码、解码 libavutil,util,通用音视频工具,像素、IO、时间等工具 libavfilter,filter,过滤器,可以用作音视频特效处理 libavdevice,device,设备

    2024年02月11日
    浏览(37)
  • MediaCodec 低延时解码

    我们在使用Android的硬解进行解码时,如果是Android11以上则可以使用其特性低延迟,谷歌官方文档 以下是Android 11支持的低时延特性: ANGLE支持:Android 11引入了ANGLE(Almost Native Graphics Layer Engine)支持,它是一个开源的跨平台图形引擎,可以将OpenGL ES和Vulkan API转换为DirectX API。

    2024年02月14日
    浏览(33)
  • 从原理到实践:音视频编码与解码技术解析

    1.1 引言 音视频编码与解码技术在现代数字媒体领域中扮演着至关重要的角色。随着互联网和移动设备的快速发展,音视频数据的传输和处理变得越来越普遍和重要。理解音视频编码与解码的原理与实践对于开发高质量、高效率的音视频应用程序至关重要。 1.2 音视频编码与解

    2024年02月03日
    浏览(51)
  • android硬编解码MediaCodec

    一 mediacodec简介        MediaCodec 类可以用来访问底层媒体编解码器,即编码器/解码器的组件。 它是 Android 底层多媒体支持架构的一部分(通常与 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起使用)。        编解码器可以处理三类数据:压

    2023年04月12日
    浏览(85)
  • 直播平台源码开发提高直播质量的关键:视频编码和解码技术

      在互联网日益发展的今天,直播平台成为人们互联网生活的主力军,直播平台功能的多样化与智能化使我们的生活有了极大地改变,比如短视频功能,它让我们既可以随时随地去发布自己所拍摄到的东西让世界各地的用户看到,也能让我们能看到世界各地所发生的事情;再

    2024年02月15日
    浏览(56)
  • android nv21数据用mediacodec编解码

    在 Android 中使用 MediaCodec 进行 NV21 编码和解码的过程如下: 编码 NV21 数据: 解码编码后的数据: 上述代码中的变量和参数需要根据你的实际情况进行调整。此外,NV21 格式的数据需要根据具体需要进行分割和处理传入编码器和解码器。

    2024年02月13日
    浏览(37)
  • 使用 MediaCodec 在 Android 上进行硬解码

    要使用 MediaCodec 在 Android 上进行硬解码,并获取 RGBA 数据,你可以按照以下步骤进行操作: 创建 MediaExtractor 对象并设置要解码的 MP4 文件路径: 根据需要选择音频或视频轨道: 创建 MediaCodec 对象并配置解码器: 循环解码并获取 RGBA 数据: 在上述代码中,你需要根据解码器

    2024年04月24日
    浏览(35)
  • 53、RK3588测试视频编解码和 POE OAK Camera编码结合开发

    基本思想:一直想学rk3588的视频编解码,奈何没有设备,最近获得机会,利用空闲时间好好研究一番,正好手中的深度相机oak camera支持视频编码,逐想用软解编码和瑞芯微的mpp硬解码去走一波,本实验使用的poe-rj45接口和usb低电压接口测试 测试数据

    2024年02月06日
    浏览(83)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包