音声つき動画を MediaExtractor, MediaCodec, MediaSync を用いて再生するサンプルコードです。
minsdkversion=24 ですが、一箇所だけですので、minsdkversion=23 に簡単に落とせます。
実機で検証することをおすすめします。Android Emulator ではコーデックの不足等により、正しく動かないことがあります。
CC0
音声つき動画を MediaExtractor, MediaCodec, MediaSync を用いて再生するサンプルコードです。
minsdkversion=24 ですが、一箇所だけですので、minsdkversion=23 に簡単に落とせます。
実機で検証することをおすすめします。Android Emulator ではコーデックの不足等により、正しく動かないことがあります。
CC0
package com.example.mediaprac; | |
import androidx.appcompat.app.AppCompatActivity; | |
import android.os.Bundle; | |
import android.os.Handler; | |
import android.view.Surface; | |
import android.view.SurfaceView; | |
import android.view.TextureView; | |
import java.io.IOException; | |
public class MainActivity extends AppCompatActivity { | |
MyMedia mMedia = null; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_main); | |
final SurfaceView sv = findViewById(R.id.surfaceView1); | |
final Surface s = sv.getHolder().getSurface(); | |
mMedia = new MyMedia(s, sv); | |
new Thread(new Runnable() { | |
@Override | |
public void run() { | |
mMedia.initialize(); | |
mMedia.run(); | |
} | |
}).start(); | |
} | |
} |
package com.example.mediaprac; | |
import android.media.AudioAttributes; | |
import android.media.AudioFormat; | |
import android.media.AudioTrack; | |
import android.media.MediaCodec; | |
import android.media.MediaCodecInfo; | |
import android.media.MediaCodecList; | |
import android.media.MediaExtractor; | |
import android.media.MediaFormat; | |
import android.media.MediaSync; | |
import android.media.PlaybackParams; | |
import android.media.SyncParams; | |
import android.os.Handler; | |
import android.util.Log; | |
import android.view.Surface; | |
import android.view.View; | |
import androidx.annotation.NonNull; | |
import java.io.IOException; | |
import java.nio.ByteBuffer; | |
public class MyMedia { | |
static final String TAG = "MyMedia"; | |
private String mContentUri = "https://ia600603.us.archive.org/30/items/Tears-of-Steel/tears_of_steel_1080p.mp4"; | |
// private String mContentUri = "https://scontent-nrt1-1.cdninstagram.com/v/t50.2886-16/41638619_239560346735367_5701419805668761311_n.mp4?_nc_ht=scontent-nrt1-1.cdninstagram.com&_nc_cat=103&_nc_ohc=NN0ddOIY3fUAX81uvbP&oe=5E5992B3&oh=f3f3097a6daa345e82fc5c0f12c7cb24"; | |
private boolean mRunning; | |
private View mView; | |
private Surface mGivenSurface, mSurface; | |
private AudioTrack mAudioTrack; | |
private MediaSync mSync; | |
private MediaExtractor mVideoExtractor, mAudioExtractor; | |
private MediaCodec mVideoCodec, mAudioCodec; | |
static private MediaExtractor createExtractor(String uri){ | |
MediaExtractor extractor = new MediaExtractor(); | |
try { | |
extractor.setDataSource(uri); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
extractor.release(); | |
return null; | |
} | |
return extractor; | |
} | |
static private int[] findTrackAVIndex(MediaExtractor extractor) { | |
int[] ii = new int[2]; | |
ii[0] = -1; // video | |
ii[1] = -1; // audio | |
for (int i = 0, n = extractor.getTrackCount(); i < n; ++i) { | |
MediaFormat format = extractor.getTrackFormat(i); | |
String mime = format.getString(MediaFormat.KEY_MIME); | |
Log.d(TAG, "track " + i + " : key_mime = " + mime); | |
if (mime == null) continue; | |
if (mime.contains("video")) | |
ii[0] = i; | |
else if (mime.contains("audio")) | |
ii[1] = i; | |
} | |
return ii; | |
} | |
static private MediaCodec createMediaCodec(MediaFormat format){ | |
assert format != null; | |
String codecName = new MediaCodecList(MediaCodecList.ALL_CODECS).findDecoderForFormat(format); | |
MediaCodec codec = null; | |
try { | |
String mime = format.getString(MediaFormat.KEY_MIME); | |
Log.d(TAG, "try to create a codec mime="+mime+" codecName="+codecName); | |
if (codecName != null) | |
codec = MediaCodec.createByCodecName(codecName); | |
else if (mime != null) | |
codec = MediaCodec.createDecoderByType(mime); // may be throw IllegalArgumentException | |
} catch (IOException e) { | |
e.printStackTrace(); | |
return null; | |
} | |
return codec; | |
} | |
static private MediaCodec createVideoDecoder(MediaFormat format, Surface surface) { | |
MediaCodec codec = createMediaCodec(format); | |
if (codec == null) | |
return null; | |
codec.configure(format, surface, null, 0); | |
return codec; | |
} | |
static private MediaCodec createAudioDecoder(MediaFormat format) { | |
MediaCodec codec = createMediaCodec(format); | |
if (codec == null) | |
return null; | |
codec.configure(format, null, null, 0); | |
return codec; | |
} | |
static private AudioFormat createAudioFormatFromMediaFormat(MediaFormat mediaFormat) { | |
AudioFormat.Builder b = new AudioFormat.Builder(); | |
if (mediaFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) { | |
b.setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)); | |
} | |
else { | |
Log.w(TAG, "createAudioFormatFormMediaFormat: not found KEY_SAMPLE_RATE"); | |
b.setSampleRate(44100); | |
} | |
if (mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)) { | |
b.setEncoding(mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING)); | |
} | |
else { | |
Log.w(TAG, "createAudioFormatFormMediaFormat: not found KEY_PCM_ENCODING"); | |
b.setEncoding(AudioFormat.ENCODING_PCM_16BIT); | |
} | |
if (mediaFormat.containsKey(MediaFormat.KEY_CHANNEL_MASK)) { | |
b.setChannelMask(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_MASK)); | |
} | |
else { | |
Log.w(TAG, "createAudioFormatFormMediaFormat: not found KEY_CHANNEL_MASK"); | |
b.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO); | |
} | |
return b.build(); | |
} | |
static private AudioTrack createAudioTrack(AudioFormat audioFormat) { | |
int size = AudioTrack.getMinBufferSize(audioFormat.getSampleRate(), | |
audioFormat.getChannelMask(), audioFormat.getEncoding()); | |
return new AudioTrack.Builder() | |
.setAudioAttributes(new AudioAttributes.Builder() | |
.setUsage(AudioAttributes.USAGE_MEDIA) | |
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) | |
.build()) | |
.setAudioFormat(audioFormat) | |
.setTransferMode(AudioTrack.MODE_STREAM) | |
.setBufferSizeInBytes(size) | |
.build(); | |
} | |
static private void dumpMediaCodecList() { | |
MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS); | |
for (MediaCodecInfo mci : mcl.getCodecInfos()) { | |
for (String type : mci.getSupportedTypes()) { | |
Log.d(TAG, mci.getName() + " supports " + type); | |
} | |
} | |
} | |
public MyMedia(Surface surface, View view) { | |
assert surface != null; | |
mRunning = false; | |
mView = view; | |
mGivenSurface = surface; | |
dumpMediaCodecList(); | |
} | |
public boolean initialize() { | |
Log.d(TAG, "[1]initialize"); | |
mRunning = false; | |
MediaExtractor extractor = createExtractor(mContentUri); | |
if (extractor == null) { | |
Log.w(TAG, "createExtractor failed."); | |
return false; | |
} | |
mSync = new MediaSync(); | |
mSync.setPlaybackParams(new PlaybackParams().setSpeed(0.f)); | |
mSync.setSurface(mGivenSurface); | |
mSurface = mSync.createInputSurface(); | |
int videoTrackIndex, audioTrackIndex; | |
{ | |
int[] ii = findTrackAVIndex(extractor); | |
videoTrackIndex = ii[0]; | |
audioTrackIndex = ii[1]; | |
if (videoTrackIndex < 0) { | |
Log.w(TAG, "videoTrack not found"); | |
return false; | |
} | |
if (audioTrackIndex < 0) { | |
Log.w(TAG, "audioTrack not found"); | |
return false; | |
} | |
Log.d(TAG, "videoTrackIndex=" + videoTrackIndex + " audioTrackIndex="+audioTrackIndex); | |
} | |
MediaFormat audioMediaFormat = extractor.getTrackFormat(audioTrackIndex); | |
mVideoExtractor = createExtractor(mContentUri); | |
assert mVideoExtractor != null; | |
mVideoExtractor.selectTrack(videoTrackIndex); | |
mVideoCodec = createVideoDecoder(extractor.getTrackFormat(videoTrackIndex), mSurface); | |
mAudioExtractor = createExtractor(mContentUri); | |
assert mAudioExtractor != null; | |
mAudioExtractor.selectTrack(audioTrackIndex); | |
mAudioCodec = createAudioDecoder(extractor.getTrackFormat(audioTrackIndex)); | |
mAudioTrack = createAudioTrack(createAudioFormatFromMediaFormat(audioMediaFormat)); | |
mSync.setAudioTrack(mAudioTrack); | |
mSync.setSyncParams(new SyncParams() | |
.setSyncSource(SyncParams.SYNC_SOURCE_SYSTEM_CLOCK)); | |
Log.d(TAG, "[2]initialize"); | |
return true; | |
} | |
public void release() { | |
Log.d(TAG, "[1]release"); | |
mRunning = false; | |
mVideoExtractor.release(); | |
mVideoCodec.release(); | |
mAudioExtractor.release(); | |
mAudioCodec.release(); | |
mAudioTrack.release(); | |
mSync.release(); | |
Log.d(TAG, "[2]release"); | |
} | |
void run() { | |
Log.d(TAG, "[1]run"); | |
mRunning = true; | |
mVideoCodec.setCallback(new CodecCallback(mVideoExtractor, mSync, false)); | |
mAudioCodec.setCallback(new CodecCallback(mAudioExtractor, mSync, true)); | |
mSync.setCallback(new MediaSync.Callback() { | |
@Override | |
public void onAudioBufferConsumed(MediaSync sync, ByteBuffer audioBuffer, int bufferId) { | |
Log.d(TAG, "onAudioBufferConsumed " + bufferId); | |
mAudioCodec.releaseOutputBuffer(bufferId, true); | |
} | |
}, null);// This needs to be done since sync is paused on creation. | |
mVideoCodec.start(); | |
mAudioCodec.start(); | |
new Thread(new Runnable() { | |
@Override | |
public void run() { | |
Log.d(TAG, "ready..."); | |
try { | |
Thread.sleep(2000); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
mSync.setPlaybackParams(new PlaybackParams().setSpeed(1.f)); | |
Log.d(TAG, "play !!!!!!"); | |
; | |
} | |
}).start(); | |
Log.d(TAG, "[2]run"); | |
} | |
class CodecCallback extends MediaCodec.Callback { | |
MediaExtractor mExtractor; | |
MediaSync mSync; | |
boolean mIsAudio; | |
CodecCallback(MediaExtractor extractor, MediaSync sync, boolean isAudio) { | |
mExtractor = extractor; | |
mSync = sync; | |
mIsAudio = isAudio; | |
} | |
private String getTag() { | |
return mIsAudio ? "Audio" : "Video"; | |
} | |
@Override | |
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int index) { | |
Log.d(TAG, "onInputBufferAvailable " + getTag() + " i=" + index); | |
ByteBuffer buffer = mediaCodec.getInputBuffer(index); | |
if (buffer == null) { | |
Log.w(TAG, "codec buffer is null"); | |
return; | |
} | |
int size = mExtractor.readSampleData(buffer, 0); | |
if (size < 0) { | |
Log.w(TAG, "track empty"); | |
return; | |
} | |
long time = mExtractor.getSampleTime(); | |
Log.d(TAG, "sampleTime=" + time); | |
mediaCodec.queueInputBuffer( | |
index, 0, size, | |
time, 0); | |
mExtractor.advance(); | |
} | |
@Override | |
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index, @NonNull MediaCodec.BufferInfo bufferInfo) { | |
Log.d(TAG, "onOutputBufferAvailable " + getTag() + " i=" + index + " timeMs="+bufferInfo.presentationTimeUs/1000); | |
if (mIsAudio) { | |
ByteBuffer buffer = mediaCodec.getOutputBuffer(index); | |
if (buffer == null) { | |
Log.w(TAG, "buffer is null"); | |
return; | |
} | |
mSync.queueAudio(buffer, index, bufferInfo.presentationTimeUs); | |
} | |
else { | |
mediaCodec.releaseOutputBuffer(index, bufferInfo.presentationTimeUs*1000); // renderTimestampNs | |
} | |
} | |
@Override | |
public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) { | |
Log.e(TAG, "codec: onError " + getTag()); | |
e.printStackTrace(); | |
} | |
@Override | |
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) { | |
Log.w(TAG, "codec: onOutputFormatChanged " + getTag()); | |
} | |
} | |
} |