테스트 mp4파일 받기
com.android.grafika
public static void initialize(Context context) {
ContentManager mgr = getInstance();
synchronized (sLock) {
if (!mgr.mInitialized) {
+ mgr.mFilesDir = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);
- // mgr.mFilesDir = context.getFilesDir();
mgr.mContent = new ArrayList<Content>();
mgr.mInitialized = true;
}
}
}
app 인터널 storage를 external로 변경
앱 재실행 후 파일 다운로드 표시 끝나면 android file explore를 이용해서
sdcard> Android > data > com.google.grafica > files > Movies
이하를 조회하면,
- gen-eight-rects.mp4
- gen-sliders.mp4
파일 확인 가능함
MediaFormat
비디오 파일(예, mp4)등의 header를 분석한 결과 MediaExtractor
를 통해서 정보를 추출할 수 있음
아래 소스는 android app 내의 res>raw 이하에 비디오나 오디오 파일을 위치 시켰을 때 uri 방식으로 수신 할 수 있는 코드이다.
관련 수정사항은 extractor.setDataSource(ctx,uri,null);
이 부분 전후를 보면 된다.
해당 메서드를 요청하는 방법은 Activity에서 아래와 같이 call 하면 된다
player = new MoviePlayer(getApplicationContext(), Uri.parse("android.resource://"+getPackageName()+"/raw/gen_eight_rects"), surface, callback);
MoviePlayer.java
public MoviePlayer(Context ctx, Uri uri, Surface outputSurface, FrameCallback frameCallback) throws IOException {
mContext = ctx;
mUri = uri;
// mSourceFile = sourceFile;
mOutputSurface = outputSurface;
mFrameCallback = frameCallback;
// Pop the file open and pull out the video characteristics.
// TODO: consider leaving the extractor open. Should be able to just seek back to
// the start after each iteration of play. Need to rearrange the API a bit --
// currently play() is taking an all-in-one open+work+release approach.
MediaExtractor extractor = null;
try {
extractor = new MediaExtractor();
extractor.setDataSource(ctx,uri,null);
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + mSourceFile);
}
extractor.selectTrack(trackIndex);
MediaFormat format = extractor.getTrackFormat(trackIndex);
mVideoWidth = format.getInteger(MediaFormat.KEY_WIDTH);
mVideoHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
if (VERBOSE) {
Log.d(TAG, "Video size is " + mVideoWidth + "x" + mVideoHeight);
}
} finally {
if (extractor != null) {
extractor.release();
}
}
}
/**
* Returns the width, in pixels, of the video.
*/
public int getVideoWidth() {
return mVideoWidth;
}
/**
* Returns the height, in pixels, of the video.
*/
public int getVideoHeight() {
return mVideoHeight;
}
/**
* Sets the loop mode. If true, playback will loop forever.
*/
public void setLoopMode(boolean loopMode) {
mLoop = loopMode;
}
/**
* Asks the player to stop. Returns without waiting for playback to halt.
* <p>
* Called from arbitrary thread.
*/
public void requestStop() {
mIsStopRequested = true;
}
/**
* Decodes the video stream, sending frames to the surface.
* <p>
* Does not return until video playback is complete, or we get a "stop" signal from
* frameCallback.
*/
public void play() throws IOException {
MediaExtractor extractor = null;
MediaCodec decoder = null;
boolean fileBased = true;
// The MediaExtractor error messages aren't very useful. Check to see if the input
// file exists so we can throw a better one if it's not there.
if (mSourceFile != null && !mSourceFile.canRead()) {
throw new FileNotFoundException("Unable to read " + mSourceFile);
} else {
fileBased = false;
}
try {
extractor = new MediaExtractor();
// extractor.setDataSource(mSourceFile.toString());
extractor.setDataSource(mContext,mUri,null);
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + mSourceFile);
}
extractor.selectTrack(trackIndex);
MediaFormat format = extractor.getTrackFormat(trackIndex);
// Create a MediaCodec decoder, and configure it with the MediaFormat from the
// extractor. It's very important to use the format from the extractor because
// it contains a copy of the CSD-0/CSD-1 codec-specific data chunks.
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, mOutputSurface, null, 0);
decoder.start();
doExtract(extractor, trackIndex, decoder, mFrameCallback);
} finally {
// release everything we grabbed
if (decoder != null) {
decoder.stop();
decoder.release();
decoder = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
}
MediaFormat format = extractor.getTrackFormat(trackIndex);
이 부분이 track 으로 부터 Media의 정보를 얻어오는 부분이다.
Audio / Video 공통 포멧 정보
Name | Value Type | Description |
---|---|---|
KEY_MIME |
String | The type of the format. |
KEY_MAX_INPUT_SIZE |
Integer | optional, maximum size of a buffer of input data |
KEY_BIT_RATE |
Integer | encoder-only, desired bitrate in bits/second |
Video 포멧 정보
Name | Value Type | Description |
---|---|---|
KEY_WIDTH |
Integer | |
KEY_HEIGHT |
Integer | |
KEY_COLOR_FORMAT |
Integer | set by the user for encoders, readable in the output format of decoders |
KEY_FRAME_RATE |
Integer or Float | required for encoders, optional for decoders |
KEY_CAPTURE_RATE |
Integer | |
KEY_I_FRAME_INTERVAL |
Integer (or Float) | encoder-only, time-interval between key frames. Float support added in Build.VERSION_CODES.N_MR1 |
KEY_INTRA_REFRESH_PERIOD |
Integer | encoder-only, optional |
KEY_LATENCY |
Integer | encoder-only, optional |
KEY_MAX_WIDTH |
Integer | decoder-only, optional, max-resolution width |
KEY_MAX_HEIGHT |
Integer | decoder-only, optional, max-resolution height |
KEY_REPEAT_PREVIOUS_FRAME_AFTER |
Long | encoder in surface-mode only, optional |
KEY_PUSH_BLANK_BUFFERS_ON_STOP |
Integer(1) | decoder rendering to a surface only, optional |
KEY_TEMPORAL_LAYERING |
String | encoder only, optional, temporal-layering schema |
오디오 포멧정보
Name | Value Type | Description |
---|---|---|
KEY_CHANNEL_COUNT |
Integer | |
KEY_SAMPLE_RATE |
Integer | |
KEY_PCM_ENCODING |
Integer | optional |
KEY_IS_ADTS |
Integer | optional, if decoding AAC audio content, setting this key to 1 indicates that each audio frame is prefixed by the ADTS header. |
KEY_AAC_PROFILE |
Integer | encoder-only, optional, if content is AAC audio, specifies the desired profile. |
KEY_AAC_SBR_MODE |
Integer | encoder-only, optional, if content is AAC audio, specifies the desired SBR mode. |
KEY_AAC_DRC_TARGET_REFERENCE_LEVEL |
Integer | decoder-only, optional, if content is AAC audio, specifies the target reference level. |
KEY_AAC_ENCODED_TARGET_LEVEL |
Integer | decoder-only, optional, if content is AAC audio, specifies the target reference level used at encoder. |
KEY_AAC_DRC_BOOST_FACTOR |
Integer | decoder-only, optional, if content is AAC audio, specifies the DRC boost factor. |
KEY_AAC_DRC_ATTENUATION_FACTOR |
Integer | decoder-only, optional, if content is AAC audio, specifies the DRC attenuation factor. |
KEY_AAC_DRC_HEAVY_COMPRESSION |
Integer | decoder-only, optional, if content is AAC audio, specifies whether to use heavy compression. |
KEY_AAC_MAX_OUTPUT_CHANNEL_COUNT |
Integer | decoder-only, optional, if content is AAC audio, specifies the maximum number of channels the decoder outputs. |
KEY_AAC_DRC_EFFECT_TYPE |
Integer | decoder-only, optional, if content is AAC audio, specifies the MPEG-D DRC effect type to use. |
KEY_CHANNEL_MASK |
Integer | optional, a mask of audio channel assignments |
KEY_FLAC_COMPRESSION_LEVEL |
Integer | encoder-only, optional, if content is FLAC audio, specifies the desired compression level. |
자막정보
Name | Value Type | Description |
---|---|---|
KEY_MIME |
String | The type of the format. |
KEY_LANGUAGE |
String | The language of the content. |
이미지 정보
Name | Value Type | Description |
---|---|---|
KEY_MIME |
String | The type of the format. |
KEY_WIDTH |
Integer | |
KEY_HEIGHT |
Integer | |
KEY_COLOR_FORMAT |
Integer | set by the user for encoders, readable in the output format of decoders |
KEY_TILE_WIDTH |
Integer | required if the image has grid |
KEY_TILE_HEIGHT |
Integer | required if the image has grid |
KEY_GRID_ROWS |
Integer | required if the image has grid |
KEY_GRID_COLUMNS |
Integer | required if the image has grid |
MediaCodec
미디어 코덱은 미디어 데이터의 제공자와 미디어 데이터를 소비하는 소비자의 중간에 위치하는 존재이다.
즉,
(미디어 데이터 제공자) --(encoded data stream)--> [MediaCodec] --(decoded data stream) -->(소비자)
형태로 나타나게 된다.
MoviePalyer 소스 코드를 보자면
public void play() throws IOException {
MediaExtractor extractor = null;
MediaCodec decoder = null;
boolean fileBased = true;
// The MediaExtractor error messages aren't very useful. Check to see if the input
// file exists so we can throw a better one if it's not there.
if (mSourceFile != null && !mSourceFile.canRead()) {
throw new FileNotFoundException("Unable to read " + mSourceFile);
} else {
fileBased = false;
}
try {
extractor = new MediaExtractor();
// extractor.setDataSource(mSourceFile.toString());
extractor.setDataSource(mContext,mUri,null);
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + mSourceFile);
}
extractor.selectTrack(trackIndex);
MediaFormat format = extractor.getTrackFormat(trackIndex);
// Create a MediaCodec decoder, and configure it with the MediaFormat from the
// extractor. It's very important to use the format from the extractor because
// it contains a copy of the CSD-0/CSD-1 codec-specific data chunks.
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, mOutputSurface, null, 0);
decoder.start();
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, mOutputSurface, null, 0);
decoder.start();
이 부분이 MediaCodec을 생산자와 소비자의 사이에 위치 시키는 역할을 한다.
위치를 시킨후 실행 시키는 코드가 decoder.start()
이다
start
public void start ()
환경 설정이 완료 된 이후 start를 실행 하면 비로소, 비동기적으로 생산자로 부터 버퍼를 읽어서 디코딩 처리 하기 시작한다.
Buffer얻어 오기
Syncronized
API21(롤리팝) 이후 deprecated 된 방법 (grafika)
grafika의 Movie Player는 이 방법을 사용해서 Data Stream을 접근 하고 있다.
버퍼의 형식은 ByteBuffer[]
start() Call 이후 부터 버퍼를 얻어 오는 방식은 getInput과 OutputBuffers를 사용하는 방법이다. 양수의 buffer id를 기준으로 얻어온다.
dequeueInputBuffer()
,dequeueOutputBuffer()
는 비동기 스레드에 상태를 확인하기 위한 api이다. 비록 Syncronized 방식이라고 해도 이점은 변하지 않는다.파라메터로 timeoutUs 가 사용되는데, 해당 값은 상위 상태 변화를 확인하기 위해 기다리는 시간이다. 해당 시간을 기다려도 state의 변화가 생기지 않는다면, 해당 media data는 invalidate 처리 되게 된다. timeoutUs가 0이하면 무한이 기다리게 된다. 0이면 기다리지 않고 즉각 return 처리 한다.
MoviePlayer.java
private void doExtract(MediaExtractor extractor, int trackIndex, MediaCodec decoder,
FrameCallback frameCallback) {
final int TIMEOUT_USEC = 10000;
ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
int inputChunk = 0;
long firstInputTimeNsec = -1;
boolean outputDone = false;
boolean inputDone = false;
while (!outputDone) {
if (VERBOSE) Log.d(TAG, "loop");
if (mIsStopRequested) {
Log.d(TAG, "Stop requested");
return;
}
// Feed more data to the decoder.
if (!inputDone) {
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
if (inputBufIndex >= 0) {
if (firstInputTimeNsec == -1) {
firstInputTimeNsec = System.nanoTime();
}
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
// Read the sample data into the ByteBuffer. This neither respects nor
// updates inputBuf's position, limit, etc.
int chunkSize = extractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {
// End of stream -- send empty frame with EOS flag set.
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
if (VERBOSE) Log.d(TAG, "sent input EOS");
} else {
if (extractor.getSampleTrackIndex() != trackIndex) {
Log.w(TAG, "WEIRD: got sample from track " +
extractor.getSampleTrackIndex() + ", expected " + trackIndex);
}
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
presentationTimeUs, 0 /*flags*/);
if (VERBOSE) {
Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +
chunkSize);
}
inputChunk++;
extractor.advance();
}
} else {
if (VERBOSE) Log.d(TAG, "input buffer not available");
}
}
if (!outputDone) {
int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) Log.d(TAG, "no output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not important for us, since we're using Surface
if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = decoder.getOutputFormat();
if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
} else if (decoderStatus < 0) {
throw new RuntimeException(
"unexpected result from decoder.dequeueOutputBuffer: " +
decoderStatus);
} else { // decoderStatus >= 0
if (firstInputTimeNsec != 0) {
// Log the delay from the first buffer of input to the first buffer
// of output.
long nowNsec = System.nanoTime();
Log.d(TAG, "startup lag " + ((nowNsec-firstInputTimeNsec) / 1000000.0) + " ms");
firstInputTimeNsec = 0;
}
boolean doLoop = false;
if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +
" (size=" + mBufferInfo.size + ")");
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (VERBOSE) Log.d(TAG, "output EOS");
if (mLoop) {
doLoop = true;
} else {
outputDone = true;
}
}
boolean doRender = (mBufferInfo.size != 0);
// As soon as we call releaseOutputBuffer, the buffer will be forwarded
// to SurfaceTexture to convert to a texture. We can't control when it
// appears on-screen, but we can manage the pace at which we release
// the buffers.
if (doRender && frameCallback != null) {
frameCallback.preRender(mBufferInfo.presentationTimeUs);
}
decoder.releaseOutputBuffer(decoderStatus, doRender);
if (doRender && frameCallback != null) {
frameCallback.postRender();
}
if (doLoop) {
Log.d(TAG, "Reached EOS, looping");
extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
inputDone = false;
decoder.flush(); // reset decoder state
frameCallback.loopReset();
}
}
}
}
}
이 메서드는 video가 멈추거나 끝날때 까지 루프를 돌게 된다.
ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
getInputBuffers (input surface를 사용하는 경우 절대로 사용하면 안됨)
해당 메서드는 21이후로 더이상 사용되지 않는다. getInputBuffer(int)
를 대신 사용한다.
본 메서드를 이용해서 byte stream을 읽어 오는데 일단 한번 읽어진 buffer는 재사용 되지 않는다.
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
사용 가능한 데이터의 index(위치)를 알려준다.
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
앞서 읽어온 inputbuffer에서 사용 가능한 데이터의 index의 bytebuffer를 얻어온다.
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
presentationTimeUs, 0 /*flags*/);
inputDone = true;
화면에 표시하기(decode) 위한 buffer를 지정한다.
여기서 presentationTimeUs는 이후 화면에 표시할때 나오는 timestamp와 동일 값이 된다.
해당 메서드를 Call 한 이후부터는 output이 가능한 단계 까지 while 문을 돌면서 기다리게 된다.
inputDone = true;
은 decode를 위한 버퍼를 채웠다는 flag처리이다.
이후 outputDone
이 처리 되어야 한다.
if (!outputDone) {...}
바이트를 입력 했으니 이후는 입력된 바이트가 정상적으로 decode되어서 읽을 수 있으면 된다.
int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
이 부분을 확인하는 코드이다.
dequeueOutputBuffer
decoded가 완료 되었는지 여부를 확인 하는 코드로써, 다음 3가지 중 하나의 리턴을 갖게 된다.
NFO_TRY_AGAIN_LATER
, INFO_OUTPUT_FORMAT_CHANGED
, INFO_OUTPUT_BUFFERS_CHANGED
해당 value는 모두 음수의 값을 갖고 있다.
이외 양수의 값이 있다면 decode가 완료 되었다고 생각해도 된다.
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) Log.d(TAG, "no output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not important for us, since we're using Surface
if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = decoder.getOutputFormat();
if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
} else if (decoderStatus < 0) {
throw new RuntimeException(
"unexpected result from decoder.dequeueOutputBuffer: " +
decoderStatus);
} else { // decoderStatus >= 0
decodeStatus가 양수일때 처리 하는 가능 핵심 적인 코드는 다음이다.
decoder.releaseOutputBuffer(decoderStatus, doRender);
releaseOutputBuffer
output surface에 그려질 decode된 byte buffer를 돌려주는 행위를 하는 코드이다. dequeueOutputBuffer
에서 지정해준 index를 기준으로 화면에 rendering을 처리하게 된다. doRender가 true이면 output surface에 최우선 적으로 데이터를 send하게 된다. 일단 해당 버퍼가 사용되고 나면 surface는 codec에게 더이상 재활용 하지 말것을 요청하게 된다. 해당 처리에 대한 결과로는 MediaCodec.Callback
에 속해 있는 onOutputBufferAvailable
가 trigger 된다.
해당 decode가 완료된 이후 surfacetexture view 쪽으로 화면의 redraw를 다음 리스너를 통해 알려준다.
TextureView.SurfaceTextureListener
onSurfaceTextureUpdated
SurfaceTexture에 있는 updateTexImage가 Call되면 이 method가 call되게 되는데, MediaCodec이 surface에 변화를 줄때마다 해당 시점을 얻어 오고 싶다면, 본 리스너를 등록 하면 된다.
PlayMovieActivity.java
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
Matrix txform = new Matrix();
mTextureView.getTransform(txform);
mRotateValue %= 360;
txform.postRotate(mRotateValue += 1);
mTextureView.setTransform(txform);
Log.d(TAG, "onSurfaceTextureUpdated = " + mRotateValue);
}
grafika 에서는 adjustAspectRatio
라는 메서드를 통해서 동영상의 비율을 조정하게 되는데, 해당 코드의 일부분을 상위와 같이 onSurfaceTextureUpdated
에 넣게 되면 동영상이 돌아가는 모습을 확인 할 수있게 된다.
Surface에 대한 수정을 처리 하고 싶을 때는 onSurfaceTextureAvailable
이나 onSurfaceTextureUpdated
시점에 처리 해주면 된다.
만약 송출되는 Surface의 화면을 mirror(flip) 처리 하고자 한다면 PlayMovieActivity 내 adjustAspectRatio
메서드 마지막에 mTextureView.setScaleX(-1);
처리를 하면 된다.
Matrix의 setScale(-1,1)을 통한 flip은 성공하지 못했다.
'Android' 카테고리의 다른 글
Android TV Application 최초 설정 (0) | 2020.10.08 |
---|---|
AOSP system app install (0) | 2020.08.20 |
android memory leak 처리 (0) | 2020.03.27 |
android graphic architecture (0) | 2020.03.27 |
android ART GC Log (0) | 2020.03.27 |