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