前回の記事Android のオーディオとビデオのコーデック (1) - MediaCodec 入門 で、MediaCodec の知覚的な理解がすでにあるこの章では、MediaCodec のデコード機能を学習します。
この章の効果は次のとおりです。
1.同期デコード
MediaCodec の動作原理と動作手順をよりよく理解するために、まず同期デコード方式を使用してローカル ビデオをデコードします。
1.1 ビデオ パラメータを取得する
まず、MP4 形式などのエンコード済みのビデオを準備する必要があります.MediaCodec を介してビデオの MediaFormat 情報を取得できます.MediaExtractor に慣れていない場合は、Android のオーディオおよびビデオ開発を使用できます(5) – MediaExtractor を使用してオーディオとビデオを分離し、MediaMuxer を使用して新しいビデオを合成 (オーディオとビデオの同期) して学習します。
MediaExtractor を実装して具体的にビデオを分析し、ビデオ データを取得する MyExtractor を定義します。
public MyExtractor(String path) {
try {
mediaExtractor = new MediaExtractor();
// 设置数据源
mediaExtractor.setDataSource(path);
} catch (IOException e) {
e.printStackTrace();
}
//拿到所有的轨道
int count = mediaExtractor.getTrackCount();
for (int i = 0; i < count; i++) {
//根据下标拿到 MediaFormat
MediaFormat format = mediaExtractor.getTrackFormat(i);
//拿到 mime 类型
String mime = format.getString(MediaFormat.KEY_MIME);
//拿到视频轨
if (mime.startsWith("video")) {
videoTrackId = i;
videoFormat = format;
} else if (mime.startsWith("audio")) {
//拿到音频轨
audioTrackId = i;
audioFormat = format;
}
}
}
public void selectTrack(int trackId){
mediaExtractor.selectTrack(trackId);
}
/**
* 读取一帧的数据
*
* @param buffer
* @return
*/
public int readBuffer(ByteBuffer buffer) {
//先清空数据
buffer.clear();
//选择要解析的轨道
// mediaExtractor.selectTrack(video ? videoTrackId : audioTrackId);
//读取当前帧的数据
int buffercount = mediaExtractor.readSampleData(buffer, 0);
if (buffercount < 0) {
return -1;
}
//记录当前时间戳
curSampleTime = mediaExtractor.getSampleTime();
//记录当前帧的标志位
curSampleFlags = mediaExtractor.getSampleFlags();
//进入下一帧
mediaExtractor.advance();
return buffercount;
}
最初に、selectTrack を使用してビデオまたはオーディオを分析するかどうかを指定し、次に、mediaExtractor.readSampleData(buffer, 0); を使用する readBuffer メソッドを使用して現在のビデオのバッファを取得し、mediaExtractor.advance( を介して次のものを取得します。 ) フレーム データ。
1.2 デコード処理
前の章で、MediaCodec のデコードは次の 2 つの図に基づいていると述べました。
MediaCodec の動作図
MediaCodec 状態図
この 2 つの画像の処理に慣れていない場合は、Android のオーディオとビデオのコーデック (1) - MediaCodec 予備調査 をお読みください。
後続のオーディオ デコーディングを容易にするために、ここではビデオとオーディオを解析するための基本クラスを定義します。ビデオは同期的に解析されるため、スレッドで解析する必要があるため、Runnable を継承します。
/**
* 解码基类,用于解码音视频
*/
abstract class BaseDecode implements Runnable {
final static int VIDEO = 1;
final static int AUDIO = 2;
//等待时间
final static int TIME_US = 1000;
MediaFormat mediaFormat;
MediaCodec mediaCodec;
MyExtractor extractor;
private boolean isDone;
public BaseDecode() {
try {
//获取 MediaExtractor
extractor = new MyExtractor(Constants.VIDEO_PATH);
//判断是音频还是视频
int type = decodeType();
//拿到音频或视频的 MediaFormat
mediaFormat = (type == VIDEO ? extractor.getVideoFormat() : extractor.getAudioFormat());
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
//选择要解析的轨道
extractor.selectTrack(type == VIDEO ? extractor.getVideoTrackId() : extractor.getAudioTrackId());
//创建 MediaCodec
mediaCodec = MediaCodec.createDecoderByType(mime);
//由子类去配置
configure();
//开始工作,进入编解码状态
mediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ビデオまたはビデオに応じて異なる MediaFormats が取得され、MIME タイプに応じて MediaCodec が作成されることがわかります。
このようにして、最初のステップが作成されます. 次に、上記の状態から未初期化状態になります. 次に、configure メソッドを呼び出して構成済み状態に入る必要があります. このステップは、ビデオなどのサブクラスによって完了されます:
@Override
void configure() {
mediaCodec.configure(mediaFormat, new Surface(mTextureView.getSurfaceTexture()), null, 0);
}
ビデオを再生するための現在の MediaFormat と Surface が mediaCodec.configure() によって構成されており、TextureView がここで使用されていることがわかります。次に、MediaCodec の start() を呼び出して Executing 状態に入り、エンコードとデコードを開始します。
1.3 ビデオのデコード
デコードプロセスはこの画像に基づいています
1.3.1 入力
BaseDecode は Runnable を継承していると前述したので、デコード処理は run メソッド内にあります。
@Override
public void run() {
try {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
//编码
while (!isDone) {
/**
* 延迟 TIME_US 等待拿到空的 input buffer下标,单位为 us
* -1 表示一直等待,知道拿到数据,0 表示立即返回
*/
int inputBufferId = mediaCodec.dequeueInputBuffer(TIME_US);
if (inputBufferId > 0) {
//拿到 可用的,空的 input buffer
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
if (inputBuffer != null) {
/**
* 通过 mediaExtractor.readSampleData(buffer, 0) 拿到视频的当前帧的buffer
* 通过 mediaExtractor.advance() 拿到下一帧
*/
int size = extractor.readBuffer(inputBuffer);
//解析数据
if (size >= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
size,
extractor.getSampleTime(),
extractor.getSampleFlags()
);
} else {
//结束,传递 end-of-stream 标志
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
);
isDone = true;
}
}
}
//解码输出交给子类
boolean isFinish = handleOutputData(info);
if (isFinish){
break;
}
}
done();
} catch (Exception e) {
e.printStackTrace();
}
}
protected void done(){
try {
isDone = true;
//释放 mediacodec
mediaCodec.stop();
mediaCodec.release();
//释放 MediaExtractor
extractor.release();
} catch (Exception e) {
e.printStackTrace();
}
}
abstract boolean handleOutputData(MediaCodec.BufferInfo info);
while ループの連続デコードでは、上記のコードは次のプロセスを実行します。
MediaCodec からアイドル バッファを取得します ビデオからビデオの現在のフレームのデータを取得し、MediaCodec のバッファに入力します mediaCodec.queueInputBuffer() を使用して、バッファのデータを MediaCodec に渡してデコードします
1.3.2 出力
上記の出力処理は handleOutputData() で実装され、VideoDecodeSync に渡されます. コードは次のように実装されます:
@Override
boolean handleOutputData(MediaCodec.BufferInfo info) {
//等到拿到输出的buffer下标
int outputId = mediaCodec.dequeueOutputBuffer(info, TIME_US);
if (outputId >= 0){
//释放buffer,并渲染到 Surface 中
mediaCodec.releaseOutputBuffer(outputId, true);
}
// 在所有解码后的帧都被渲染后,就可以停止播放了
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.e(TAG, "zsr OutputBuffer BUFFER_FLAG_END_OF_STREAM");
return true;
}
return false;
}
上記のコードは、次の 2 つのことも行います。
-
出力バッファを取得する
-
releaseOutputBuffer() の 2 番目のパラメーターによって制御される、バッファーを解放し、ビデオを Surface にレンダリングします。
このようにして、ビデオのデコード部分を書きました。効果は次のとおりです。
ただし、ビデオが 2 倍の速度で再生されているように見えることがわかります。
1.3.3 正しい表示タイムスタンプ
上記の状況が発生するのはなぜですか?通常、当社のビデオ再生のフレーム レートは約 30、つまり 30fps であり、33.33ms ごとに 1 フレームが再生されますが、フレームをデコードする時間は約数ミリ秒で、デコードされた場合はそのまま表示されます。そうしないと、ビデオが 2 倍速で再生されているように見えます。だったら30msの遅延で遊べばいい、というのが標準ではないでしょうか。はい、30fps が標準ですが、すべてのビデオが 30 であるという意味ではありません。ここでは、オーディオとビデオ、DTS と PTS の基本を学ぶ必要があります。
【学習アドレス】:FFmpeg/WebRTC/RTMP/NDK/Androidの音声・動画ストリーミングメディアの高度な開発
[記事の特典]: より多くのオーディオおよびビデオ学習パッケージ、Dachang インタビューの質問、テクニカル ビデオ、学習ロードマップを無料で受け取ることができます (C/C++、Linux、FFmpeg webRTC rtmp hls rtsp ffplay srs など) 1079654574 をクリックして参加します受け取るグループ〜
DTS (Decoding Time Stamp): デコード タイム スタンプです. このタイム スタンプの意味は、このフレームのデータをいつデコードするかをプレーヤーに伝えることです. PTS (Presentation Time Stamp): タイム スタンプを表示します. このタイム スタンプは、 The player what このフレームを再生するとき、DTS と PTS はプレーヤーの動作をガイドするために使用されますが、これらはエンコード中にエンコーダーによって生成されることに注意してください。B フレームがない場合、DTS と PTS の出力順序は同じですが、B フレームがある場合は順序が異なります。ここでは、タイムスタンプを表示する PTS だけを気にする必要があります。現在の pts タイムスタンプは、MediaCodec.BufferInfo の presentationTimeUs を介して取得できます. 単位は微妙です. 0 に対する再生開始時間です. したがって、システムの時間差を使用して、2 つのフレーム間の時間差を模倣することができます。デコードされた pts を比較すると、時間差が速い場合は遅れて Surface に出力され、そうでない場合は直接 Surface に表示されます。
これはスレッド内にあるため、Surface にレンダリングする前に Thread.sleep() を使用して実現できます。
// 用于对准视频的时间戳
private long startMs = -1;
if (outputId >= 0){
if (mStartMs == -1) {
mStartMs = System.currentTimeMillis();
}
//矫正pts
sleepRender(info, startMs);
//释放buffer,并渲染到 Surface 中
mediaCodec.releaseOutputBuffer(outputId, true);
}
#sleepRender
/**
* 数据的时间戳对齐
**/
private void sleepRender(MediaCodec.BufferInfo info, long startMs) {
/**
* 注意这里是以 0 为出事目标的,info.presenttationTimes 的单位为微秒
* 这里用系统时间来模拟两帧的时间差
*/
long ptsTimes = info.presentationTimeUs / 1000;
long systemTimes = System.currentTimeMillis() - startMs;
long timeDifference = ptsTimes - systemTimes;
// 如果当前帧比系统时间差快了,则延时以下
if (timeDifference > 0) {
try {
Thread.sleep(timeDifference);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
時間再生は正常になりました。
1.4 オーディオのデコード
映像のデコードは以上で理解できたので、音声のデコードは比較的簡単です. 新しい AudioDecodeSync クラスを作成し、BaseDecode を継承し、configure メソッドで MediaCodec を構成します. Surface は必要ないので、null を渡すだけです.
@Override
void configure() {
mediaCodec.configure(mediaFormat, null, null, 0);
}
Surface を使用する必要はありませんが、ビデオを再生する必要がある場合は、AudioTrack を使用する必要があります. 必要がない場合は、Android のオーディオとビデオの開発 (1) - AudioRecord を使用して PCM を記録するを参照してください。 (録音); オーディオを再生するための AudioTrack。
したがって、AudioDecodeSync の構築メソッドでは、次の AudioTrack を構成する必要があります。
class AudioDecodeSync extends BaseDecode {
private int mPcmEncode;
//一帧的最小buffer大小
private final int minBufferSize;
private AudioTrack audioTrack;
public AudioDecodeSync() {
//拿到采样率
if (mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
mPcmEncode = mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING);
} else {
//默认采样率为 16bit
mPcmEncode = AudioFormat.ENCODING_PCM_16BIT;
}
//音频采样率
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
//获取视频通道数
int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
//拿到声道
int channelConfig = channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO;
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, mPcmEncode);
/**
* 设置音频信息属性
* 1.设置支持多媒体属性,比如audio,video
* 2.设置音频格式,比如 music
*/
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
/**
* 设置音频数据
* 1. 设置采样率
* 2. 设置采样位数
* 3. 设置声道
*/
AudioFormat format = new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(channelConfig)
.build();
//配置 audioTrack
audioTrack = new AudioTrack(
attributes,
format,
minBufferSize,
AudioTrack.MODE_STREAM, //采用流模式
AudioManager.AUDIO_SESSION_ID_GENERATE
);
//监听播放
audioTrack.play();
}
}
AudioTrack を取得したら、play() メソッドを使用してデータが書き込まれているかどうかを監視し、オーディオの再生を開始できます。
ハンドル出力データ:
@Override
boolean handleOutputData(MediaCodec.BufferInfo info) {
//拿到output buffer
int outputIndex = mediaCodec.dequeueOutputBuffer(info, TIME_US);
ByteBuffer outputBuffer;
if (outputIndex >= 0) {
outputBuffer = mediaCodec.getOutputBuffer(outputIndex);
//写数据到 AudioTrack 只,实现音频播放
audioTrack.write(outputBuffer, info.size, AudioTrack.WRITE_BLOCKING);
mediaCodec.releaseOutputBuffer(outputIndex, false);
}
// 在所有解码后的帧都被渲染后,就可以停止播放了
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.e(TAG, "zsr OutputBuffer BUFFER_FLAG_END_OF_STREAM");
return true;
}
return false;
}
音声は正常に再生されており、音声のタイム スタンプは比較的連続しているため、早送りの意味がないことがわかります。したがって、それを修正する必要はありません。
1.5 オーディオとビデオの同期
では、オーディオとビデオを同期するにはどうすればよいでしょうか。実際、2 つのスレッドを開いて同時に再生させるだけなので、難しくはありません。
if (mExecutorService.isShutdown()) {
mExecutorService = Executors.newFixedThreadPool(2);
}
mVideoSync = new VideoDecodeSync();
mAudioDecodeSync = new AudioDecodeSync();
mExecutorService.execute(mVideoSync);
mExecutorService.execute(mAudioDecodeSync);
}
2.非同期デコード
5.0 以降、google は非同期デコードによる MediaCodec の使用を推奨しています。これも非常に簡単で、setCallback メソッドを使用するだけです。たとえば、上記のビデオを分析するには、次の手順を実行できます。
MediaExtractor を使用してビデオを解析し、MediaFormat を取得し、MediaCodec.setCallback() メソッドを使用して mediaCodec.configure() および mediaCodec.start() を呼び出してデコードを開始します。したがって、コードは次のようになります。
class AsyncDecode {
MediaFormat mediaFormat;
MediaCodec mediaCodec;
MyExtractor extractor;
public AsyncDecode() {
try {
//解析视频,拿到 mediaformat
extractor = new MyExtractor(Constants.VIDEO_PATH);
mediaFormat = (extractor.getVideoFormat());
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
extractor.selectTrack(extractor.getVideoTrackId());
mediaCodec = MediaCodec.createDecoderByType(mime);
} catch (IOException e) {
e.printStackTrace();
}
}
private void start() {
//异步解码
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
ByteBuffer inputBuffer = codec.getInputBuffer(index);
int size = extractor.readBuffer(inputBuffer);
if (size >= 0) {
codec.queueInputBuffer(
index,
0,
size,
extractor.getSampleTime(),
extractor.getSampleFlags()
);
handler.sendEmptyMessage(1);
} else {
//结束
codec.queueInputBuffer(
index,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
);
}
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
mediaCodec.releaseOutputBuffer(index, true);
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
codec.reset();
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
}
});
//需要在 setCallback 之后,配置 configure
mediaCodec.configure(mediaFormat, new Surface(mTextureView.getSurfaceTexture()), null, 0);
//开始解码
mediaCodec.start();
}
}
非同期デコードを使用するプロセスは、基本的に同期デコードのプロセスと同じですが、同期コードでは、
int inputBufferId = mediaCodec.dequeueInputBuffer(TIME_US);
未使用の入力バッファーの添え字を取得するのを待機するには、非同期で、コールバックを使用します。
void onInputBufferAvailable(@NonNull MediaCodec codec, int index)
入力バッファの添え字を取得します。事故がなければ、ビデオはすでに再生を開始していますが、上記の問題、つまり、倍速で再生されるという問題が発生する場合もあります.これは、PTSの問題が修正されていないためです.
私たちのコードはメイン スレッドで実行されるため、Thread.sleep() は確実にフリーズしますが、HandlerThread または他のスレッドを使用して解析できるので、ここでは投稿しません。詳しくはソース コードを見てみましょう。
3、参考:
[Android のオーディオとビデオの開発モンスター アップグレード: オーディオとビデオのハード デコードの記事] 3. オーディオとビデオの再生: オーディオとビデオの同期 - ショート ブック Android ビデオ処理 MediaCodec-3 - ビデオの再生 MediaCodec | Android 開発者
元のリンク: Android オーディオおよびビデオ コーデック (2) -- MediaCodec デコード (同期および非同期)_mediacodec 非同期デコード_夏至の稲穂のブログ - CSDN ブログ