1/*
2 * Copyright (C) 2011 The Android Open Source Project
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
18package android.filterpacks.videosrc;
19
20import android.content.Context;
21import android.content.res.AssetFileDescriptor;
22import android.filterfw.core.Filter;
23import android.filterfw.core.FilterContext;
24import android.filterfw.core.Frame;
25import android.filterfw.core.FrameFormat;
26import android.filterfw.core.GenerateFieldPort;
27import android.filterfw.core.GenerateFinalPort;
28import android.filterfw.core.GLFrame;
29import android.filterfw.core.MutableFrameFormat;
30import android.filterfw.core.ShaderProgram;
31import android.filterfw.format.ImageFormat;
32import android.graphics.SurfaceTexture;
33import android.media.MediaPlayer;
34import android.net.Uri;
35import android.opengl.Matrix;
36import android.view.Surface;
37
38import java.io.IOException;
39import java.lang.IllegalArgumentException;
40
41import android.util.Log;
42
43/**
44 * @hide
45 */
46public class MediaSource extends Filter {
47
48    /** User-visible parameters */
49
50    /** The source URL for the media source. Can be an http: link to a remote
51     * resource, or a file: link to a local media file
52     */
53    @GenerateFieldPort(name = "sourceUrl", hasDefault = true)
54    private String mSourceUrl = "";
55
56    /** An open asset file descriptor to a local media source. Default is null */
57    @GenerateFieldPort(name = "sourceAsset", hasDefault = true)
58    private AssetFileDescriptor mSourceAsset = null;
59
60    /** The context for the MediaPlayer to resolve the sourceUrl.
61     * Make sure this is set before the sourceUrl to avoid unexpected result.
62     * If the sourceUrl is not a content URI, it is OK to keep this as null. */
63    @GenerateFieldPort(name = "context", hasDefault = true)
64    private Context mContext = null;
65
66    /** Whether the media source is a URL or an asset file descriptor. Defaults
67     * to false.
68     */
69    @GenerateFieldPort(name = "sourceIsUrl", hasDefault = true)
70    private boolean mSelectedIsUrl = false;
71
72    /** Whether the filter will always wait for a new video frame, or whether it
73     * will output an old frame again if a new frame isn't available. Defaults
74     * to true.
75     */
76    @GenerateFinalPort(name = "waitForNewFrame", hasDefault = true)
77    private boolean mWaitForNewFrame = true;
78
79    /** Whether the media source should loop automatically or not. Defaults to
80     * true.
81     */
82    @GenerateFieldPort(name = "loop", hasDefault = true)
83    private boolean mLooping = true;
84
85    /** Volume control. Currently sound is piped directly to the speakers, so
86     * this defaults to mute.
87     */
88    @GenerateFieldPort(name = "volume", hasDefault = true)
89    private float mVolume = 0.f;
90
91    /** Orientation. This controls the output orientation of the video. Valid
92     * values are 0, 90, 180, 270
93     */
94    @GenerateFieldPort(name = "orientation", hasDefault = true)
95    private int mOrientation = 0;
96
97    private MediaPlayer mMediaPlayer;
98    private GLFrame mMediaFrame;
99    private SurfaceTexture mSurfaceTexture;
100    private ShaderProgram mFrameExtractor;
101    private MutableFrameFormat mOutputFormat;
102    private int mWidth, mHeight;
103
104    // Total timeouts will be PREP_TIMEOUT*PREP_TIMEOUT_REPEAT
105    private static final int PREP_TIMEOUT = 100; // ms
106    private static final int PREP_TIMEOUT_REPEAT = 100;
107    private static final int NEWFRAME_TIMEOUT = 100; //ms
108    private static final int NEWFRAME_TIMEOUT_REPEAT = 10;
109
110    // This is an identity shader; not using the default identity
111    // shader because reading from a SurfaceTexture requires the
112    // GL_OES_EGL_image_external extension.
113    private final String mFrameShader =
114            "#extension GL_OES_EGL_image_external : require\n" +
115            "precision mediump float;\n" +
116            "uniform samplerExternalOES tex_sampler_0;\n" +
117            "varying vec2 v_texcoord;\n" +
118            "void main() {\n" +
119            "  gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" +
120            "}\n";
121
122    // The following transforms enable rotation of the decoded source.
123    // These are multiplied with the transform obtained from the
124    // SurfaceTexture to get the final transform to be set on the media source.
125    // Currently, given a device orientation, the MediaSource rotates in such a way
126    // that the source is displayed upright. A particular use case
127    // is "Background Replacement" feature in the Camera app
128    // where the MediaSource rotates the source to align with the camera feed and pass it
129    // on to the backdropper filter. The backdropper only does the blending
130    // and does not have to do any rotation
131    // (except for mirroring in case of front camera).
132    // TODO: Currently the rotations are spread over a bunch of stages in the
133    // pipeline. A cleaner design
134    // could be to cast away all the rotation in a separate filter or attach a transform
135    // to the frame so that MediaSource itself need not know about any rotation.
136    private static final float[] mSourceCoords_0 = { 1, 1, 0, 1,
137                                                     0, 1, 0, 1,
138                                                     1, 0, 0, 1,
139                                                     0, 0, 0, 1 };
140    private static final float[] mSourceCoords_270 = { 0, 1, 0, 1,
141                                                      0, 0, 0, 1,
142                                                      1, 1, 0, 1,
143                                                      1, 0, 0, 1 };
144    private static final float[] mSourceCoords_180 = { 0, 0, 0, 1,
145                                                       1, 0, 0, 1,
146                                                       0, 1, 0, 1,
147                                                       1, 1, 0, 1 };
148    private static final float[] mSourceCoords_90 = { 1, 0, 0, 1,
149                                                       1, 1, 0, 1,
150                                                       0, 0, 0, 1,
151                                                       0, 1, 0, 1 };
152
153    private boolean mGotSize;
154    private boolean mPrepared;
155    private boolean mPlaying;
156    private boolean mNewFrameAvailable;
157    private boolean mOrientationUpdated;
158    private boolean mPaused;
159    private boolean mCompleted;
160
161    private final boolean mLogVerbose;
162    private static final String TAG = "MediaSource";
163
164    public MediaSource(String name) {
165        super(name);
166        mNewFrameAvailable = false;
167
168        mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
169    }
170
171    @Override
172    public void setupPorts() {
173        // Add input port
174        addOutputPort("video", ImageFormat.create(ImageFormat.COLORSPACE_RGBA,
175                                                  FrameFormat.TARGET_GPU));
176    }
177
178    private void createFormats() {
179        mOutputFormat = ImageFormat.create(ImageFormat.COLORSPACE_RGBA,
180                                           FrameFormat.TARGET_GPU);
181    }
182
183    @Override
184    protected void prepare(FilterContext context) {
185        if (mLogVerbose) Log.v(TAG, "Preparing MediaSource");
186
187        mFrameExtractor = new ShaderProgram(context, mFrameShader);
188        // SurfaceTexture defines (0,0) to be bottom-left. The filter framework
189        // defines (0,0) as top-left, so do the flip here.
190        mFrameExtractor.setSourceRect(0, 1, 1, -1);
191
192        createFormats();
193    }
194
195    @Override
196    public void open(FilterContext context) {
197        if (mLogVerbose) {
198            Log.v(TAG, "Opening MediaSource");
199            if (mSelectedIsUrl) {
200                Log.v(TAG, "Current URL is " + mSourceUrl);
201            } else {
202                Log.v(TAG, "Current source is Asset!");
203            }
204        }
205
206        mMediaFrame = (GLFrame)context.getFrameManager().newBoundFrame(
207                mOutputFormat,
208                GLFrame.EXTERNAL_TEXTURE,
209                0);
210
211        mSurfaceTexture = new SurfaceTexture(mMediaFrame.getTextureId());
212
213        if (!setupMediaPlayer(mSelectedIsUrl)) {
214          throw new RuntimeException("Error setting up MediaPlayer!");
215        }
216    }
217
218    @Override
219    public void process(FilterContext context) {
220        // Note: process is synchronized by its caller in the Filter base class
221        if (mLogVerbose) Log.v(TAG, "Processing new frame");
222
223        if (mMediaPlayer == null) {
224            // Something went wrong in initialization or parameter updates
225            throw new NullPointerException("Unexpected null media player!");
226        }
227
228        if (mCompleted) {
229            // Video playback is done, so close us down
230            closeOutputPort("video");
231            return;
232        }
233
234        if (!mPlaying) {
235            int waitCount = 0;
236            if (mLogVerbose) Log.v(TAG, "Waiting for preparation to complete");
237            while (!mGotSize || !mPrepared) {
238                try {
239                    this.wait(PREP_TIMEOUT);
240                } catch (InterruptedException e) {
241                    // ignoring
242                }
243                if (mCompleted) {
244                    // Video playback is done, so close us down
245                    closeOutputPort("video");
246                    return;
247                }
248                waitCount++;
249                if (waitCount == PREP_TIMEOUT_REPEAT) {
250                    mMediaPlayer.release();
251                    throw new RuntimeException("MediaPlayer timed out while preparing!");
252                }
253            }
254            if (mLogVerbose) Log.v(TAG, "Starting playback");
255            mMediaPlayer.start();
256        }
257
258        // Use last frame if paused, unless just starting playback, in which case
259        // we want at least one valid frame before pausing
260        if (!mPaused || !mPlaying) {
261            if (mWaitForNewFrame) {
262                if (mLogVerbose) Log.v(TAG, "Waiting for new frame");
263
264                int waitCount = 0;
265                while (!mNewFrameAvailable) {
266                    if (waitCount == NEWFRAME_TIMEOUT_REPEAT) {
267                        if (mCompleted) {
268                            // Video playback is done, so close us down
269                            closeOutputPort("video");
270                            return;
271                        } else {
272                            throw new RuntimeException("Timeout waiting for new frame!");
273                        }
274                    }
275                    try {
276                        this.wait(NEWFRAME_TIMEOUT);
277                    } catch (InterruptedException e) {
278                        if (mLogVerbose) Log.v(TAG, "interrupted");
279                        // ignoring
280                    }
281                    waitCount++;
282                }
283                mNewFrameAvailable = false;
284                if (mLogVerbose) Log.v(TAG, "Got new frame");
285            }
286
287            mSurfaceTexture.updateTexImage();
288            mOrientationUpdated = true;
289        }
290        if (mOrientationUpdated) {
291            float[] surfaceTransform = new float[16];
292            mSurfaceTexture.getTransformMatrix(surfaceTransform);
293
294            float[] sourceCoords = new float[16];
295            switch (mOrientation) {
296                default:
297                case 0:
298                    Matrix.multiplyMM(sourceCoords, 0,
299                                      surfaceTransform, 0,
300                                      mSourceCoords_0, 0);
301                    break;
302                case 90:
303                    Matrix.multiplyMM(sourceCoords, 0,
304                                      surfaceTransform, 0,
305                                      mSourceCoords_90, 0);
306                    break;
307                case 180:
308                    Matrix.multiplyMM(sourceCoords, 0,
309                                      surfaceTransform, 0,
310                                      mSourceCoords_180, 0);
311                    break;
312                case 270:
313                    Matrix.multiplyMM(sourceCoords, 0,
314                                      surfaceTransform, 0,
315                                      mSourceCoords_270, 0);
316                    break;
317            }
318            if (mLogVerbose) {
319                Log.v(TAG, "OrientationHint = " + mOrientation);
320                String temp = String.format("SetSourceRegion: %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f",
321                        sourceCoords[4], sourceCoords[5],sourceCoords[0], sourceCoords[1],
322                        sourceCoords[12], sourceCoords[13],sourceCoords[8], sourceCoords[9]);
323                Log.v(TAG, temp);
324            }
325            mFrameExtractor.setSourceRegion(sourceCoords[4], sourceCoords[5],
326                    sourceCoords[0], sourceCoords[1],
327                    sourceCoords[12], sourceCoords[13],
328                    sourceCoords[8], sourceCoords[9]);
329            mOrientationUpdated = false;
330        }
331
332        Frame output = context.getFrameManager().newFrame(mOutputFormat);
333        mFrameExtractor.process(mMediaFrame, output);
334
335        long timestamp = mSurfaceTexture.getTimestamp();
336        if (mLogVerbose) Log.v(TAG, "Timestamp: " + (timestamp / 1000000000.0) + " s");
337        output.setTimestamp(timestamp);
338
339        pushOutput("video", output);
340        output.release();
341
342        mPlaying = true;
343    }
344
345    @Override
346    public void close(FilterContext context) {
347        if (mMediaPlayer.isPlaying()) {
348            mMediaPlayer.stop();
349        }
350        mPrepared = false;
351        mGotSize = false;
352        mPlaying = false;
353        mPaused = false;
354        mCompleted = false;
355        mNewFrameAvailable = false;
356
357        mMediaPlayer.release();
358        mMediaPlayer = null;
359        mSurfaceTexture.release();
360        mSurfaceTexture = null;
361        if (mLogVerbose) Log.v(TAG, "MediaSource closed");
362    }
363
364    @Override
365    public void tearDown(FilterContext context) {
366        if (mMediaFrame != null) {
367            mMediaFrame.release();
368        }
369    }
370
371    // When updating the port values of the filter, users can update sourceIsUrl to switch
372    //   between using URL objects or Assets.
373    // If updating only sourceUrl/sourceAsset, MediaPlayer gets reset if the current player
374    //   uses Url objects/Asset.
375    // Otherwise the new sourceUrl/sourceAsset is stored and will be used when users switch
376    //   sourceIsUrl next time.
377    @Override
378    public void fieldPortValueUpdated(String name, FilterContext context) {
379        if (mLogVerbose) Log.v(TAG, "Parameter update");
380        if (name.equals("sourceUrl")) {
381           if (isOpen()) {
382                if (mLogVerbose) Log.v(TAG, "Opening new source URL");
383                if (mSelectedIsUrl) {
384                    setupMediaPlayer(mSelectedIsUrl);
385                }
386            }
387        } else if (name.equals("sourceAsset") ) {
388            if (isOpen()) {
389                if (mLogVerbose) Log.v(TAG, "Opening new source FD");
390                if (!mSelectedIsUrl) {
391                    setupMediaPlayer(mSelectedIsUrl);
392                }
393            }
394        } else if (name.equals("loop")) {
395            if (isOpen()) {
396                mMediaPlayer.setLooping(mLooping);
397            }
398        } else if (name.equals("sourceIsUrl")) {
399            if (isOpen()){
400                if (mSelectedIsUrl){
401                    if (mLogVerbose) Log.v(TAG, "Opening new source URL");
402                } else {
403                    if (mLogVerbose) Log.v(TAG, "Opening new source Asset");
404                }
405                setupMediaPlayer(mSelectedIsUrl);
406            }
407        } else if (name.equals("volume")) {
408            if (isOpen()) {
409                mMediaPlayer.setVolume(mVolume, mVolume);
410            }
411        } else if (name.equals("orientation") && mGotSize) {
412            if (mOrientation == 0 || mOrientation == 180) {
413                mOutputFormat.setDimensions(mWidth, mHeight);
414            } else {
415                mOutputFormat.setDimensions(mHeight, mWidth);
416            }
417            mOrientationUpdated = true;
418        }
419    }
420
421    synchronized public void pauseVideo(boolean pauseState) {
422        if (isOpen()) {
423            if (pauseState && !mPaused) {
424                mMediaPlayer.pause();
425            } else if (!pauseState && mPaused) {
426                mMediaPlayer.start();
427            }
428        }
429        mPaused = pauseState;
430    }
431
432    /** Creates a media player, sets it up, and calls prepare */
433    synchronized private boolean setupMediaPlayer(boolean useUrl) {
434        mPrepared = false;
435        mGotSize = false;
436        mPlaying = false;
437        mPaused = false;
438        mCompleted = false;
439        mNewFrameAvailable = false;
440
441        if (mLogVerbose) Log.v(TAG, "Setting up playback.");
442
443        if (mMediaPlayer != null) {
444            // Clean up existing media players
445            if (mLogVerbose) Log.v(TAG, "Resetting existing MediaPlayer.");
446            mMediaPlayer.reset();
447        } else {
448            // Create new media player
449            if (mLogVerbose) Log.v(TAG, "Creating new MediaPlayer.");
450            mMediaPlayer = new MediaPlayer();
451        }
452
453        if (mMediaPlayer == null) {
454            throw new RuntimeException("Unable to create a MediaPlayer!");
455        }
456
457        // Set up data sources, etc
458        try {
459            if (useUrl) {
460                if (mLogVerbose) Log.v(TAG, "Setting MediaPlayer source to URI " + mSourceUrl);
461                if (mContext == null) {
462                    mMediaPlayer.setDataSource(mSourceUrl);
463                } else {
464                    mMediaPlayer.setDataSource(mContext, Uri.parse(mSourceUrl.toString()));
465                }
466            } else {
467                if (mLogVerbose) Log.v(TAG, "Setting MediaPlayer source to asset " + mSourceAsset);
468                mMediaPlayer.setDataSource(mSourceAsset.getFileDescriptor(), mSourceAsset.getStartOffset(), mSourceAsset.getLength());
469            }
470        } catch(IOException e) {
471            mMediaPlayer.release();
472            mMediaPlayer = null;
473            if (useUrl) {
474                throw new RuntimeException(String.format("Unable to set MediaPlayer to URL %s!", mSourceUrl), e);
475            } else {
476                throw new RuntimeException(String.format("Unable to set MediaPlayer to asset %s!", mSourceAsset), e);
477            }
478        } catch(IllegalArgumentException e) {
479            mMediaPlayer.release();
480            mMediaPlayer = null;
481            if (useUrl) {
482                throw new RuntimeException(String.format("Unable to set MediaPlayer to URL %s!", mSourceUrl), e);
483            } else {
484                throw new RuntimeException(String.format("Unable to set MediaPlayer to asset %s!", mSourceAsset), e);
485            }
486        }
487
488        mMediaPlayer.setLooping(mLooping);
489        mMediaPlayer.setVolume(mVolume, mVolume);
490
491        // Bind it to our media frame
492        Surface surface = new Surface(mSurfaceTexture);
493        mMediaPlayer.setSurface(surface);
494        surface.release();
495
496        // Connect Media Player to callbacks
497
498        mMediaPlayer.setOnVideoSizeChangedListener(onVideoSizeChangedListener);
499        mMediaPlayer.setOnPreparedListener(onPreparedListener);
500        mMediaPlayer.setOnCompletionListener(onCompletionListener);
501
502        // Connect SurfaceTexture to callback
503        mSurfaceTexture.setOnFrameAvailableListener(onMediaFrameAvailableListener);
504
505        if (mLogVerbose) Log.v(TAG, "Preparing MediaPlayer.");
506        mMediaPlayer.prepareAsync();
507
508        return true;
509    }
510
511    private MediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedListener =
512            new MediaPlayer.OnVideoSizeChangedListener() {
513        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
514            if (mLogVerbose) Log.v(TAG, "MediaPlayer sent dimensions: " + width + " x " + height);
515            if (!mGotSize) {
516                if (mOrientation == 0 || mOrientation == 180) {
517                    mOutputFormat.setDimensions(width, height);
518                } else {
519                    mOutputFormat.setDimensions(height, width);
520                }
521                mWidth = width;
522                mHeight = height;
523            } else {
524                if (mOutputFormat.getWidth() != width ||
525                    mOutputFormat.getHeight() != height) {
526                    Log.e(TAG, "Multiple video size change events received!");
527                }
528            }
529            synchronized(MediaSource.this) {
530                mGotSize = true;
531                MediaSource.this.notify();
532            }
533        }
534    };
535
536    private MediaPlayer.OnPreparedListener onPreparedListener =
537            new MediaPlayer.OnPreparedListener() {
538        public void onPrepared(MediaPlayer mp) {
539            if (mLogVerbose) Log.v(TAG, "MediaPlayer is prepared");
540            synchronized(MediaSource.this) {
541                mPrepared = true;
542                MediaSource.this.notify();
543            }
544        }
545    };
546
547    private MediaPlayer.OnCompletionListener onCompletionListener =
548            new MediaPlayer.OnCompletionListener() {
549        public void onCompletion(MediaPlayer mp) {
550            if (mLogVerbose) Log.v(TAG, "MediaPlayer has completed playback");
551            synchronized(MediaSource.this) {
552                mCompleted = true;
553            }
554        }
555    };
556
557    private SurfaceTexture.OnFrameAvailableListener onMediaFrameAvailableListener =
558            new SurfaceTexture.OnFrameAvailableListener() {
559        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
560            if (mLogVerbose) Log.v(TAG, "New frame from media player");
561            synchronized(MediaSource.this) {
562                if (mLogVerbose) Log.v(TAG, "New frame: notify");
563                mNewFrameAvailable = true;
564                MediaSource.this.notify();
565                if (mLogVerbose) Log.v(TAG, "New frame: notify done");
566            }
567        }
568    };
569
570}
571