├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── myth │ │ └── frameplayer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── myth │ │ │ └── frameplayer │ │ │ ├── FramePlayer.java │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── myth │ └── frameplayer │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── show.gif /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android ### 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | signing.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | # proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | ### Android Patch ### 37 | gen-external-apklibs 38 | 39 | 40 | ### Intellij ### 41 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 42 | 43 | *.iml 44 | 45 | ## Directory-based project format: 46 | .idea/ 47 | # if you remove the above rule, at least ignore the following: 48 | 49 | # User-specific stuff: 50 | # .idea/workspace.xml 51 | # .idea/tasks.xml 52 | # .idea/dictionaries 53 | # .idea/shelf 54 | 55 | # Sensitive or high-churn files: 56 | # .idea/dataSources.ids 57 | # .idea/dataSources.xml 58 | # .idea/sqlDataSources.xml 59 | # .idea/dynamic.xml 60 | # .idea/uiDesigner.xml 61 | 62 | # Gradle: 63 | # .idea/gradle.xml 64 | # .idea/libraries 65 | 66 | # Mongo Explorer plugin: 67 | # .idea/mongoSettings.xml 68 | 69 | ## File-based project format: 70 | *.ipr 71 | *.iws 72 | 73 | ## Plugin-specific files: 74 | 75 | # IntelliJ 76 | /out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Crashlytics plugin (for Android Studio and IntelliJ) 85 | com_crashlytics_export_strings.xml 86 | crashlytics.properties 87 | crashlytics-build.properties 88 | fabric.properties 89 | 90 | .DS_Store 91 | ### Java ### 92 | *.class 93 | 94 | app/bin/ 95 | build 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frameplayer 2 | a video player which can control frame rate base on MediaCodec 3 | 4 | ![image](show.gif) 5 | 6 | License 7 | ======= 8 | 9 | Copyright (c) 2015 myandy 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.myth.frameplayer" 8 | minSdkVersion 16 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.1.0' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/mi/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/myth/frameplayer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.myth.frameplayer; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.myth.frameplayer", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/myth/frameplayer/FramePlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.myth.frameplayer; 18 | 19 | import android.media.AudioFormat; 20 | import android.media.AudioManager; 21 | import android.media.AudioTrack; 22 | import android.media.MediaCodec; 23 | import android.media.MediaExtractor; 24 | import android.media.MediaFormat; 25 | import android.os.AsyncTask; 26 | import android.os.Build; 27 | import android.os.Handler; 28 | import android.os.Looper; 29 | import android.os.Message; 30 | import android.util.Log; 31 | import android.view.Surface; 32 | 33 | import java.io.File; 34 | import java.io.FileNotFoundException; 35 | import java.io.IOException; 36 | import java.nio.ByteBuffer; 37 | import java.util.Timer; 38 | import java.util.TimerTask; 39 | 40 | import static android.media.MediaExtractor.SEEK_TO_PREVIOUS_SYNC; 41 | 42 | 43 | public class FramePlayer implements Runnable { 44 | private static final String TAG = FramePlayer.class.getSimpleName(); 45 | 46 | 47 | private static final int MSG_PLAY_START = 0; 48 | 49 | private static final int MSG_PLAY_PROGRESS = 1; 50 | 51 | 52 | private static final boolean VERBOSE = true; 53 | 54 | private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); 55 | private ByteBuffer inputBuffer; 56 | 57 | final int TIMEOUT_USEC = 10000; 58 | private AudioTrack audioTrack; 59 | 60 | public void setSourceFile(File sourceFile) { 61 | this.sourceFile = sourceFile; 62 | } 63 | 64 | private File sourceFile; 65 | private Surface mOutputSurface; 66 | private int mVideoWidth; 67 | private int mVideoHeight; 68 | 69 | private MediaExtractor mAudioExtractor; 70 | private MediaExtractor mMediaExtractor; 71 | private MediaCodec mAudioCodec; 72 | private MediaCodec mMediaCodec; 73 | private int mTrackIndex; 74 | private int mAudioTrackIndex; 75 | private Thread mThread; 76 | private LocalHandler mLocalHandler; 77 | 78 | public void setPlayListener(PlayListener playListener) { 79 | this.playListener = playListener; 80 | } 81 | 82 | private PlayListener playListener; 83 | 84 | public void setFrameInterval(int frameRate) { 85 | if (frameRate < 1 || frameRate > 100) { 86 | throw new IllegalArgumentException("frame rate must between 1 to 100"); 87 | } 88 | this.mFrameInterval = 1000 / frameRate; 89 | } 90 | 91 | private int mFrameInterval; 92 | private int videoFrameRate; 93 | 94 | 95 | public FramePlayer(Surface outputSurface) { 96 | mOutputSurface = outputSurface; 97 | } 98 | 99 | private int frame; 100 | private Timer timer; 101 | private TimerTask timerTask; 102 | 103 | private long duration; 104 | private boolean seekOffsetFlag = false; 105 | private boolean isLoop = false; 106 | private boolean hadPlay = false; 107 | private int seekOffset = 0; 108 | private boolean doStop = false; 109 | private AudioPlayTask mAudioPlayTask; 110 | 111 | private volatile boolean isRunning; 112 | 113 | public void nextFrame() { 114 | mLocalHandler.sendMessage(mLocalHandler.obtainMessage(MSG_PLAY_PROGRESS, frame++, 0)); 115 | } 116 | 117 | private class ProgressTimerTask extends java.util.TimerTask { 118 | @Override 119 | public void run() { 120 | if (isRunning) { 121 | mLocalHandler.sendMessage(mLocalHandler.obtainMessage(MSG_PLAY_PROGRESS, frame++, 0)); 122 | } 123 | } 124 | } 125 | 126 | 127 | @Override 128 | public void run() { 129 | // Establish a Looper for this thread, and define a Handler for it. 130 | Looper.prepare(); 131 | mLocalHandler = new LocalHandler(); 132 | Looper.loop(); 133 | } 134 | 135 | private class LocalHandler extends Handler { 136 | @Override 137 | public void handleMessage(Message msg) { 138 | int what = msg.what; 139 | Log.d(TAG, "handleMessage:" + what); 140 | switch (what) { 141 | case MSG_PLAY_START: 142 | try { 143 | play(); 144 | } catch (IOException e) { 145 | e.printStackTrace(); 146 | } 147 | break; 148 | case MSG_PLAY_PROGRESS: 149 | doExtract(msg.arg1); 150 | break; 151 | default: 152 | throw new RuntimeException("Unknown msg " + what); 153 | } 154 | } 155 | } 156 | 157 | 158 | /** 159 | * Creates a new thread, and starts execution of the player. 160 | */ 161 | public void execute() { 162 | mThread = new Thread(this, "Movie Player"); 163 | mThread.start(); 164 | } 165 | 166 | 167 | public void start() { 168 | stop(); 169 | frame = 0; 170 | isRunning = true; 171 | mLocalHandler.sendEmptyMessage(MSG_PLAY_START); 172 | } 173 | 174 | public void stop() { 175 | if (timer != null) { 176 | timer.cancel(); 177 | timerTask.cancel(); 178 | timer = null; 179 | timerTask = null; 180 | isRunning = false; 181 | } 182 | if (mAudioPlayTask != null) { 183 | mAudioPlayTask.cancel(true); 184 | } 185 | doStop = true; 186 | seekOffset = 0; 187 | seekOffsetFlag = false; 188 | } 189 | 190 | 191 | public boolean isRunning() { 192 | return isRunning; 193 | } 194 | 195 | 196 | public void pause() { 197 | if (isRunning) { 198 | isRunning = false; 199 | } 200 | } 201 | 202 | public void resume() { 203 | if (!isRunning) { 204 | isRunning = true; 205 | } 206 | } 207 | 208 | 209 | /** 210 | * Returns the width, in pixels, of the video. 211 | */ 212 | public int getVideoWidth() { 213 | return mVideoWidth; 214 | } 215 | 216 | /** 217 | * Returns the height, in pixels, of the video. 218 | */ 219 | public int getVideoHeight() { 220 | return mVideoHeight; 221 | } 222 | 223 | 224 | private void play() throws IOException { 225 | if (!sourceFile.canRead()) { 226 | throw new FileNotFoundException("Unable to read " + sourceFile); 227 | } 228 | try { 229 | destroyExtractor(); 230 | mMediaExtractor = new MediaExtractor(); 231 | mMediaExtractor.setDataSource(sourceFile.toString()); 232 | mTrackIndex = selectTrack(mMediaExtractor); 233 | if (mTrackIndex < 0) { 234 | throw new RuntimeException("No video track found in " + sourceFile); 235 | } 236 | MediaFormat format = mMediaExtractor.getTrackFormat(mTrackIndex); 237 | videoFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE); 238 | if (mFrameInterval == 0) { 239 | mFrameInterval = 1000 / videoFrameRate; 240 | } 241 | String mime = format.getString(MediaFormat.KEY_MIME); 242 | duration = format.getLong(MediaFormat.KEY_DURATION); 243 | 244 | 245 | mAudioExtractor = new MediaExtractor(); 246 | mAudioExtractor.setDataSource(sourceFile.toString()); 247 | mAudioTrackIndex = selectAudioTrack(mAudioExtractor); 248 | 249 | //only support pcm 250 | if (mAudioTrackIndex != -1) { 251 | relaxResources(true); 252 | MediaFormat audioFormat = mMediaExtractor.getTrackFormat(mAudioTrackIndex); 253 | String audioMime = audioFormat.getString(MediaFormat.KEY_MIME); 254 | try { 255 | // 实例化一个指定类型的解码器,提供数据输出 256 | mAudioCodec = MediaCodec.createDecoderByType(audioMime); 257 | } catch (IOException e) { 258 | e.printStackTrace(); 259 | } 260 | 261 | mAudioCodec.configure(audioFormat, null /* surface */, null /* crypto */, 0 /* flags */); 262 | mAudioCodec.start(); 263 | 264 | 265 | int channels = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); 266 | int sampleRate = (int) (1.0f * audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) * (1000 / videoFrameRate) / mFrameInterval); 267 | int channelConfiguration = channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO; 268 | audioTrack = new AudioTrack( 269 | AudioManager.STREAM_MUSIC, 270 | sampleRate, 271 | channelConfiguration, 272 | AudioFormat.ENCODING_PCM_16BIT, 273 | AudioTrack.getMinBufferSize( 274 | sampleRate, 275 | channelConfiguration, 276 | AudioFormat.ENCODING_PCM_16BIT 277 | ), 278 | AudioTrack.MODE_STREAM 279 | ); 280 | 281 | //开始play,等待write发出声音 282 | audioTrack.play(); 283 | mAudioExtractor.selectTrack(mAudioTrackIndex); 284 | } 285 | 286 | 287 | mMediaExtractor.selectTrack(mTrackIndex); 288 | mMediaCodec = MediaCodec.createDecoderByType(mime); 289 | mMediaCodec.configure(format, mOutputSurface, null, 0); 290 | mMediaCodec.start(); 291 | 292 | timer = new Timer(); 293 | timerTask = new ProgressTimerTask(); 294 | timer.schedule(timerTask, 0, mFrameInterval); 295 | 296 | mAudioPlayTask = new AudioPlayTask(); 297 | mAudioPlayTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 298 | 299 | } catch (Exception e) { 300 | Log.e(TAG, e.toString()); 301 | destroyExtractor(); 302 | } 303 | } 304 | 305 | 306 | private void destroyExtractor() { 307 | if (mMediaCodec != null) { 308 | mMediaCodec.stop(); 309 | mMediaCodec.release(); 310 | mMediaCodec = null; 311 | } 312 | if (mMediaExtractor != null) { 313 | mMediaExtractor.release(); 314 | mMediaExtractor = null; 315 | } 316 | } 317 | 318 | private void relaxResources(Boolean release) { 319 | if (mAudioCodec != null) { 320 | if (release) { 321 | mAudioCodec.stop(); 322 | mAudioCodec.release(); 323 | mAudioCodec = null; 324 | } 325 | 326 | } 327 | if (audioTrack != null) { 328 | if (!doStop) 329 | audioTrack.flush(); 330 | audioTrack.release(); 331 | audioTrack = null; 332 | } 333 | } 334 | 335 | 336 | /** 337 | * Selects the video track, if any. 338 | * 339 | * @return the track index, or -1 if no video track is found. 340 | */ 341 | private static int selectTrack(MediaExtractor mMediaExtractor) { 342 | // Select the first video track we find, ignore the rest. 343 | int numTracks = mMediaExtractor.getTrackCount(); 344 | for (int i = 0; i < numTracks; i++) { 345 | MediaFormat format = mMediaExtractor.getTrackFormat(i); 346 | String mime = format.getString(MediaFormat.KEY_MIME); 347 | if (mime.startsWith("video/")) { 348 | if (VERBOSE) { 349 | Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format); 350 | } 351 | return i; 352 | } 353 | } 354 | 355 | return -1; 356 | } 357 | 358 | private int selectAudioTrack(MediaExtractor mMediaExtractor) { 359 | for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) { 360 | MediaFormat format = mMediaExtractor.getTrackFormat(i); 361 | if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) { 362 | return i; 363 | } 364 | } 365 | return -1; 366 | } 367 | 368 | private long curPosition; 369 | 370 | private void doAudio() { 371 | 372 | ByteBuffer[] codecInputBuffers; 373 | ByteBuffer[] codecOutputBuffers; 374 | 375 | 376 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 377 | 378 | codecInputBuffers = mAudioCodec.getInputBuffers(); 379 | // 解码后的数据 380 | codecOutputBuffers = mAudioCodec.getOutputBuffers(); 381 | 382 | // 解码 383 | boolean sawInputEOS = false; 384 | boolean sawOutputEOS = false; 385 | int noOutputCounter = 0; 386 | int noOutputCounterLimit = 50; 387 | 388 | int inputBufIndex; 389 | doStop = false; 390 | while (!sawOutputEOS && noOutputCounter < noOutputCounterLimit && !doStop) { 391 | 392 | if (!isRunning) { 393 | try { 394 | //防止死循环ANR 395 | Thread.sleep(500); 396 | } catch (InterruptedException e) { 397 | e.printStackTrace(); 398 | } 399 | continue; 400 | } 401 | 402 | noOutputCounter++; 403 | if (!sawInputEOS) { 404 | if (seekOffsetFlag) { 405 | seekOffsetFlag = false; 406 | mAudioExtractor.seekTo(seekOffset, SEEK_TO_PREVIOUS_SYNC); 407 | } 408 | 409 | inputBufIndex = mAudioCodec.dequeueInputBuffer(TIMEOUT_USEC); 410 | 411 | if (inputBufIndex >= 0) { 412 | ByteBuffer dstBuf = codecInputBuffers[inputBufIndex]; 413 | 414 | int sampleSize = 415 | mAudioExtractor.readSampleData(dstBuf, 0 /* offset */); 416 | 417 | long presentationTimeUs = 0; 418 | 419 | if (sampleSize < 0) { 420 | sawInputEOS = true; 421 | sampleSize = 0; 422 | } else { 423 | presentationTimeUs = mAudioExtractor.getSampleTime(); 424 | } 425 | curPosition = presentationTimeUs; 426 | mAudioCodec.queueInputBuffer( 427 | inputBufIndex, 428 | 0 /* offset */, 429 | sampleSize, 430 | presentationTimeUs, 431 | sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0); 432 | 433 | 434 | if (!sawInputEOS) { 435 | mAudioExtractor.advance(); 436 | } 437 | } else { 438 | Log.e(TAG, "inputBufIndex " + inputBufIndex); 439 | } 440 | } 441 | 442 | // decode to PCM and push it to the AudioTrack player 443 | // 解码数据为PCM 444 | int res = mAudioCodec.dequeueOutputBuffer(info, TIMEOUT_USEC); 445 | 446 | if (res >= 0) { 447 | if (info.size > 0) { 448 | noOutputCounter = 0; 449 | } 450 | int outputBufIndex = res; 451 | ByteBuffer buf = codecOutputBuffers[outputBufIndex]; 452 | 453 | final byte[] chunk = new byte[info.size]; 454 | buf.get(chunk); 455 | buf.clear(); 456 | if (chunk.length > 0 && audioTrack != null && !doStop) { 457 | //播放 458 | audioTrack.write(chunk, 0, chunk.length); 459 | hadPlay = true; 460 | } 461 | //释放 462 | mAudioCodec.releaseOutputBuffer(outputBufIndex, false /* render */); 463 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 464 | sawOutputEOS = true; 465 | } 466 | } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 467 | codecOutputBuffers = mAudioCodec.getOutputBuffers(); 468 | } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 469 | MediaFormat oformat = mAudioCodec.getOutputFormat(); 470 | } else { 471 | } 472 | } 473 | 474 | relaxResources(true); 475 | 476 | doStop = true; 477 | 478 | if (sawOutputEOS) { 479 | try { 480 | if (isLoop || !hadPlay) { 481 | doAudio(); 482 | return; 483 | } 484 | } catch (Exception e) { 485 | e.printStackTrace(); 486 | } 487 | } 488 | } 489 | 490 | 491 | /** 492 | * AsyncTask that takes care of running the decode/playback loop 493 | */ 494 | private class AudioPlayTask extends AsyncTask { 495 | 496 | @Override 497 | protected Void doInBackground(Void... values) { 498 | doAudio(); 499 | return null; 500 | } 501 | 502 | @Override 503 | protected void onPreExecute() { 504 | } 505 | 506 | @Override 507 | protected void onProgressUpdate(Void... values) { 508 | } 509 | } 510 | 511 | 512 | private void doExtract(int frame) { 513 | 514 | if (mMediaCodec == null) { 515 | return; 516 | } 517 | int inputBufferIndex = 0; 518 | try { 519 | inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); 520 | } catch (Exception e) { 521 | // TODO: 17-3-21 error after onResume,in none exxcuting state 522 | return; 523 | } 524 | if (inputBufferIndex >= 0) { 525 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 526 | // 从输入队列里去空闲outputBufferfer 527 | inputBuffer = mMediaCodec.getInputBuffers()[inputBufferIndex]; 528 | } else { 529 | // SDK_INT > LOLLIPOP 530 | inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex); 531 | } 532 | if (null != inputBuffer) { 533 | int chunkSize = mMediaExtractor.readSampleData(inputBuffer, 0); 534 | if (chunkSize < 0) { 535 | if (isRunning && playListener != null) { 536 | playListener.onCompleted(); 537 | isRunning = false; 538 | destroyExtractor(); 539 | return; 540 | } 541 | mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, 0L, 542 | MediaCodec.BUFFER_FLAG_END_OF_STREAM); 543 | if (VERBOSE) Log.d(TAG, "sent input EOS"); 544 | } else { 545 | mMediaCodec.queueInputBuffer(inputBufferIndex, 0, chunkSize, 0, 0); 546 | mMediaExtractor.advance(); 547 | } 548 | } 549 | 550 | int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); 551 | Log.d(TAG, outputBufferIndex + ":outputBufferIndex"); 552 | if (outputBufferIndex >= 0) { 553 | mMediaCodec.releaseOutputBuffer(outputBufferIndex, true); 554 | if (playListener != null) { 555 | playListener.onProgress((frame + 1) * mFrameInterval * 1f / duration); 556 | } 557 | } else { 558 | Log.d(TAG, "Reached EOS, looping"); 559 | } 560 | } 561 | } 562 | 563 | public interface PlayListener { 564 | 565 | void onCompleted(); 566 | 567 | void onProgress(float progress); 568 | } 569 | 570 | 571 | } 572 | -------------------------------------------------------------------------------- /app/src/main/java/com/myth/frameplayer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.myth.frameplayer; 2 | 3 | import android.os.Bundle; 4 | import android.os.Environment; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.util.DisplayMetrics; 7 | import android.util.Log; 8 | import android.view.Surface; 9 | import android.view.SurfaceHolder; 10 | import android.view.SurfaceView; 11 | import android.view.View; 12 | import android.widget.EditText; 13 | import android.widget.RelativeLayout; 14 | import android.widget.TextView; 15 | import android.widget.Toast; 16 | 17 | import java.io.File; 18 | 19 | 20 | /** 21 | * warn: 22 | * need get file read permission first 23 | */ 24 | public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback { 25 | 26 | /** 27 | * test video path 28 | */ 29 | private static String videoPath = Environment.getExternalStorageDirectory().getPath() + "/Music8.1.mp4"; 30 | private FramePlayer framePlayer = null; 31 | 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_main); 37 | SurfaceView surfaceView = (SurfaceView) findViewById(R.id.playMovie_surface); 38 | surfaceView.getHolder().addCallback(this); 39 | 40 | //make its height same with width,should set for video size 41 | DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 42 | RelativeLayout.LayoutParams lps = (RelativeLayout.LayoutParams) surfaceView.getLayoutParams(); 43 | lps.width = (int) (displayMetrics.widthPixels - 2 * getResources().getDimension(R.dimen.activity_horizontal_margin)); 44 | lps.height = lps.width; 45 | surfaceView.setLayoutParams(lps); 46 | 47 | final EditText et = (EditText) findViewById(R.id.et); 48 | 49 | findViewById(R.id.play).setOnClickListener(new View.OnClickListener() { 50 | @Override 51 | public void onClick(View v) { 52 | if (et.getText().toString().isEmpty()) { 53 | framePlayer.start(); 54 | } else { 55 | int frame = Integer.parseInt(et.getText().toString()); 56 | if (frame >= 1 && frame <= 100) { 57 | framePlayer.setFrameInterval(frame); 58 | framePlayer.start(); 59 | } else { 60 | Toast.makeText(MainActivity.this, "每秒帧数必须在1到100之间!", Toast.LENGTH_SHORT).show(); 61 | } 62 | } 63 | } 64 | }); 65 | 66 | final TextView tv = (TextView) findViewById(R.id.control); 67 | findViewById(R.id.control).setOnClickListener(new View.OnClickListener() { 68 | @Override 69 | public void onClick(View v) { 70 | if (framePlayer.isRunning()) { 71 | framePlayer.pause(); 72 | tv.setText("RESUME"); 73 | } else { 74 | tv.setText("PAUSE"); 75 | framePlayer.resume(); 76 | } 77 | } 78 | }); 79 | 80 | findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { 81 | @Override 82 | public void onClick(View v) { 83 | framePlayer.nextFrame(); 84 | } 85 | }); 86 | } 87 | 88 | @Override 89 | protected void onPause() { 90 | super.onPause(); 91 | if (framePlayer != null) { 92 | framePlayer.pause(); 93 | } 94 | 95 | } 96 | 97 | @Override 98 | public void surfaceCreated(SurfaceHolder holder) { 99 | if (framePlayer == null) { 100 | Surface surface = holder.getSurface(); 101 | framePlayer = new FramePlayer(surface); 102 | framePlayer.setSourceFile(new File(videoPath)); 103 | framePlayer.execute(); 104 | framePlayer.setPlayListener(new FramePlayer.PlayListener() { 105 | @Override 106 | public void onCompleted() { 107 | Log.d("PlayListener", "onCompleted"); 108 | } 109 | 110 | @Override 111 | public void onProgress(float progress) { 112 | Log.d("PlayListener", "onProgress" + progress); 113 | } 114 | }); 115 | } 116 | } 117 | 118 | @Override 119 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 120 | 121 | } 122 | 123 | @Override 124 | public void surfaceDestroyed(SurfaceHolder holder) { 125 | 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 28 | 29 |