2 * Copyright (C) 2011 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package android.filterpacks.videosrc;
20 import android.content.Context;
21 import android.content.res.AssetFileDescriptor;
22 import android.filterfw.core.Filter;
23 import android.filterfw.core.FilterContext;
24 import android.filterfw.core.Frame;
25 import android.filterfw.core.FrameFormat;
26 import android.filterfw.core.FrameManager;
27 import android.filterfw.core.GenerateFieldPort;
28 import android.filterfw.core.GenerateFinalPort;
29 import android.filterfw.core.GLFrame;
30 import android.filterfw.core.KeyValueMap;
31 import android.filterfw.core.MutableFrameFormat;
32 import android.filterfw.core.NativeFrame;
33 import android.filterfw.core.Program;
34 import android.filterfw.core.ShaderProgram;
35 import android.filterfw.format.ImageFormat;
36 import android.graphics.SurfaceTexture;
37 import android.media.MediaPlayer;
38 import android.os.ConditionVariable;
39 import android.view.Surface;
41 import java.io.IOException;
42 import java.io.FileDescriptor;
43 import java.lang.IllegalArgumentException;
44 import java.util.List;
47 import android.util.Log;
52 public class MediaSource extends Filter {
54 /** User-visible parameters */
56 /** The source URL for the media source. Can be an http: link to a remote
57 * resource, or a file: link to a local media file */
58 @GenerateFieldPort(name = "sourceUrl", hasDefault = true)
59 private String mSourceUrl = "";
61 /** An open asset file descriptor to a local media source. Default is null */
62 @GenerateFieldPort(name = "sourceAsset", hasDefault = true)
63 private AssetFileDescriptor mSourceAsset = null;
65 /** Whether the media source is a URL or an asset file descriptor. Defaults to false. */
66 @GenerateFieldPort(name = "sourceIsUrl", hasDefault = true)
67 private boolean mSelectedIsUrl = false;
69 /** Whether the filter will always wait for a new video frame, or whether it
70 * will output an old frame again if a new frame isn't available. Defaults to true.
72 @GenerateFinalPort(name = "waitForNewFrame", hasDefault = true)
73 private boolean mWaitForNewFrame = true;
75 /** Whether the media source should loop automatically or not. Defaults to true. */
76 @GenerateFieldPort(name = "loop", hasDefault = true)
77 private boolean mLooping = true;
79 /** Volume control. Currently sound is piped directly to the speakers, so this defaults to mute. */
80 @GenerateFieldPort(name = "volume", hasDefault = true)
81 private float mVolume = 0.f;
83 private MediaPlayer mMediaPlayer;
84 private GLFrame mMediaFrame;
85 private SurfaceTexture mSurfaceTexture;
86 private ShaderProgram mFrameExtractor;
87 private MutableFrameFormat mOutputFormat;
88 private float[] mFrameTransform;
90 // Total timeouts will be PREP_TIMEOUT*PREP_TIMEOUT_REPEAT
91 private static final int PREP_TIMEOUT = 100; // ms
92 private static final int PREP_TIMEOUT_REPEAT = 100;
93 private static final int NEWFRAME_TIMEOUT = 100; //ms
94 private static final int NEWFRAME_TIMEOUT_REPEAT = 10;
96 private final String mFrameShader =
97 "#extension GL_OES_EGL_image_external : require\n" +
98 "precision mediump float;\n" +
99 "uniform mat4 frame_transform;\n" +
100 "uniform samplerExternalOES tex_sampler_0;\n" +
101 "varying vec2 v_texcoord;\n" +
103 " vec2 transformed_texcoord = (frame_transform * vec4(v_texcoord, 0., 1.) ).xy;" +
104 " gl_FragColor = texture2D(tex_sampler_0, transformed_texcoord);\n" +
107 private boolean mGotSize;
108 private boolean mPrepared;
109 private boolean mPlaying;
110 private boolean mNewFrameAvailable;
111 private boolean mPaused;
112 private boolean mCompleted;
114 private final boolean mLogVerbose;
115 private static final String TAG = "MediaSource";
117 public MediaSource(String name) {
119 mNewFrameAvailable = false;
120 mFrameTransform = new float[16];
122 mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
126 public void setupPorts() {
128 addOutputPort("video", ImageFormat.create(ImageFormat.COLORSPACE_RGBA,
129 FrameFormat.TARGET_GPU));
132 private void createFormats() {
133 mOutputFormat = ImageFormat.create(ImageFormat.COLORSPACE_RGBA,
134 FrameFormat.TARGET_GPU);
138 protected void prepare(FilterContext context) {
139 if (mLogVerbose) Log.v(TAG, "Preparing MediaSource");
141 mFrameExtractor = new ShaderProgram(context, mFrameShader);
142 // SurfaceTexture defines (0,0) to be bottom-left. The filter framework
143 // defines (0,0) as top-left, so do the flip here.
144 mFrameExtractor.setSourceRect(0, 1, 1, -1);
150 public void open(FilterContext context) {
152 Log.v(TAG, "Opening MediaSource");
153 if (mSelectedIsUrl) {
154 Log.v(TAG, "Current URL is " + mSourceUrl);
156 Log.v(TAG, "Current source is Asset!");
160 mMediaFrame = (GLFrame)context.getFrameManager().newBoundFrame(
162 GLFrame.EXTERNAL_TEXTURE,
165 mSurfaceTexture = new SurfaceTexture(mMediaFrame.getTextureId());
167 if (!setupMediaPlayer(mSelectedIsUrl)) {
168 throw new RuntimeException("Error setting up MediaPlayer!");
173 public void process(FilterContext context) {
174 // Note: process is synchronized by its caller in the Filter base class
175 if (mLogVerbose) Log.v(TAG, "Processing new frame");
177 if (mMediaPlayer == null) {
178 // Something went wrong in initialization or parameter updates
179 throw new NullPointerException("Unexpected null media player!");
183 // Video playback is done, so close us down
184 closeOutputPort("video");
190 if (mLogVerbose) Log.v(TAG, "Waiting for preparation to complete");
191 while (!mGotSize || !mPrepared) {
193 this.wait(PREP_TIMEOUT);
194 } catch (InterruptedException e) {
198 // Video playback is done, so close us down
199 closeOutputPort("video");
203 if (waitCount == PREP_TIMEOUT_REPEAT) {
204 mMediaPlayer.release();
205 throw new RuntimeException("MediaPlayer timed out while preparing!");
208 if (mLogVerbose) Log.v(TAG, "Starting playback");
209 mMediaPlayer.start();
212 // Use last frame if paused, unless just starting playback, in which case
213 // we want at least one valid frame before pausing
214 if (!mPaused || !mPlaying) {
215 if (mWaitForNewFrame) {
216 if (mLogVerbose) Log.v(TAG, "Waiting for new frame");
219 while (!mNewFrameAvailable) {
220 if (waitCount == NEWFRAME_TIMEOUT_REPEAT) {
222 // Video playback is done, so close us down
223 closeOutputPort("video");
226 throw new RuntimeException("Timeout waiting for new frame!");
230 this.wait(NEWFRAME_TIMEOUT);
231 } catch (InterruptedException e) {
232 if (mLogVerbose) Log.v(TAG, "interrupted");
237 mNewFrameAvailable = false;
238 if (mLogVerbose) Log.v(TAG, "Got new frame");
241 mSurfaceTexture.updateTexImage();
243 mSurfaceTexture.getTransformMatrix(mFrameTransform);
244 mFrameExtractor.setHostValue("frame_transform", mFrameTransform);
247 Frame output = context.getFrameManager().newFrame(mOutputFormat);
248 mFrameExtractor.process(mMediaFrame, output);
250 long timestamp = mSurfaceTexture.getTimestamp();
251 if (mLogVerbose) Log.v(TAG, "Timestamp: " + (timestamp / 1000000000.0) + " s");
252 output.setTimestamp(timestamp);
254 pushOutput("video", output);
261 public void close(FilterContext context) {
262 if (mMediaPlayer.isPlaying()) {
265 mMediaPlayer.release();
267 mSurfaceTexture.release();
268 mSurfaceTexture = null;
269 if (mLogVerbose) Log.v(TAG, "MediaSource closed");
273 public void tearDown(FilterContext context) {
274 if (mMediaFrame != null) {
275 mMediaFrame.release();
279 // When updating the port values of the filter, users can update sourceIsUrl to switch
280 // between using URL objects or Assets.
281 // If updating only sourceUrl/sourceAsset, MediaPlayer gets reset if the current player
282 // uses Url objects/Asset.
283 // Otherwise the new sourceUrl/sourceAsset is stored and will be used when users switch
284 // sourceIsUrl next time.
286 public void fieldPortValueUpdated(String name, FilterContext context) {
287 if (mLogVerbose) Log.v(TAG, "Parameter update");
288 if (name.equals("sourceUrl")) {
290 if (mLogVerbose) Log.v(TAG, "Opening new source URL");
291 if (mSelectedIsUrl) {
292 setupMediaPlayer(mSelectedIsUrl);
295 } else if (name.equals("sourceAsset") ) {
297 if (mLogVerbose) Log.v(TAG, "Opening new source FD");
298 if (!mSelectedIsUrl) {
299 setupMediaPlayer(mSelectedIsUrl);
302 } else if (name.equals("loop")) {
304 mMediaPlayer.setLooping(mLooping);
306 } else if (name.equals("sourceIsUrl")) {
309 if (mLogVerbose) Log.v(TAG, "Opening new source URL");
311 if (mLogVerbose) Log.v(TAG, "Opening new source Asset");
313 setupMediaPlayer(mSelectedIsUrl);
315 } else if (name.equals("volume")) {
317 mMediaPlayer.setVolume(mVolume, mVolume);
322 synchronized public void pauseVideo(boolean pauseState) {
324 if (pauseState && !mPaused) {
325 mMediaPlayer.pause();
326 } else if (!pauseState && mPaused) {
327 mMediaPlayer.start();
330 mPaused = pauseState;
333 /** Creates a media player, sets it up, and calls prepare */
334 synchronized private boolean setupMediaPlayer(boolean useUrl) {
340 mNewFrameAvailable = false;
342 if (mLogVerbose) Log.v(TAG, "Setting up playback.");
344 if (mMediaPlayer != null) {
345 // Clean up existing media players
346 if (mLogVerbose) Log.v(TAG, "Resetting existing MediaPlayer.");
347 mMediaPlayer.reset();
349 // Create new media player
350 if (mLogVerbose) Log.v(TAG, "Creating new MediaPlayer.");
351 mMediaPlayer = new MediaPlayer();
354 if (mMediaPlayer == null) {
355 throw new RuntimeException("Unable to create a MediaPlayer!");
358 // Set up data sources, etc
361 if (mLogVerbose) Log.v(TAG, "Setting MediaPlayer source to URI " + mSourceUrl);
362 mMediaPlayer.setDataSource(mSourceUrl);
364 if (mLogVerbose) Log.v(TAG, "Setting MediaPlayer source to asset " + mSourceAsset);
365 mMediaPlayer.setDataSource(mSourceAsset.getFileDescriptor(), mSourceAsset.getStartOffset(), mSourceAsset.getLength());
367 } catch(IOException e) {
368 mMediaPlayer.release();
371 throw new RuntimeException(String.format("Unable to set MediaPlayer to URL %s!", mSourceUrl), e);
373 throw new RuntimeException(String.format("Unable to set MediaPlayer to asset %s!", mSourceAsset), e);
375 } catch(IllegalArgumentException e) {
376 mMediaPlayer.release();
379 throw new RuntimeException(String.format("Unable to set MediaPlayer to URL %s!", mSourceUrl), e);
381 throw new RuntimeException(String.format("Unable to set MediaPlayer to asset %s!", mSourceAsset), e);
385 mMediaPlayer.setLooping(mLooping);
386 mMediaPlayer.setVolume(mVolume, mVolume);
388 // Bind it to our media frame
389 Surface surface = new Surface(mSurfaceTexture);
390 mMediaPlayer.setSurface(surface);
393 // Connect Media Player to callbacks
395 mMediaPlayer.setOnVideoSizeChangedListener(onVideoSizeChangedListener);
396 mMediaPlayer.setOnPreparedListener(onPreparedListener);
397 mMediaPlayer.setOnCompletionListener(onCompletionListener);
399 // Connect SurfaceTexture to callback
400 mSurfaceTexture.setOnFrameAvailableListener(onMediaFrameAvailableListener);
402 if (mLogVerbose) Log.v(TAG, "Preparing MediaPlayer.");
403 mMediaPlayer.prepareAsync();
408 private MediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedListener =
409 new MediaPlayer.OnVideoSizeChangedListener() {
410 public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
411 if (mLogVerbose) Log.v(TAG, "MediaPlayer sent dimensions: " + width + " x " + height);
413 mOutputFormat.setDimensions(width, height);
415 if (mOutputFormat.getWidth() != width ||
416 mOutputFormat.getHeight() != height) {
417 Log.e(TAG, "Multiple video size change events received!");
420 synchronized(MediaSource.this) {
422 MediaSource.this.notify();
427 private MediaPlayer.OnPreparedListener onPreparedListener =
428 new MediaPlayer.OnPreparedListener() {
429 public void onPrepared(MediaPlayer mp) {
430 if (mLogVerbose) Log.v(TAG, "MediaPlayer is prepared");
431 synchronized(MediaSource.this) {
433 MediaSource.this.notify();
438 private MediaPlayer.OnCompletionListener onCompletionListener =
439 new MediaPlayer.OnCompletionListener() {
440 public void onCompletion(MediaPlayer mp) {
441 if (mLogVerbose) Log.v(TAG, "MediaPlayer has completed playback");
442 synchronized(MediaSource.this) {
448 private SurfaceTexture.OnFrameAvailableListener onMediaFrameAvailableListener =
449 new SurfaceTexture.OnFrameAvailableListener() {
450 public void onFrameAvailable(SurfaceTexture surfaceTexture) {
451 if (mLogVerbose) Log.v(TAG, "New frame from media player");
452 synchronized(MediaSource.this) {
453 if (mLogVerbose) Log.v(TAG, "New frame: notify");
454 mNewFrameAvailable = true;
455 MediaSource.this.notify();
456 if (mLogVerbose) Log.v(TAG, "New frame: notify done");