为什么要学习音视频
核心竞争力,高端人才相当缺乏,技术迭代慢,
为什么音视频学不好
资料比较少,音视频最难的地方在于编码,没有形成完整的体系
关于音视频编码
上
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;
}
}
这是我拍摄的视频
这是我播放的视频
文章来源:https://www.toymoban.com/news/detail-402315.html
解决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模板网!