1 /* 2 * Copyright (C) 2017 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 package com.android.setupwizardlib.view; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.SurfaceTexture; 23 import android.graphics.drawable.Animatable; 24 import android.media.MediaPlayer; 25 import android.os.Build.VERSION_CODES; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.RawRes; 28 import android.support.annotation.VisibleForTesting; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.view.Surface; 32 import android.view.TextureView; 33 import android.view.View; 34 35 import com.android.setupwizardlib.R; 36 37 /** 38 * A view for displaying videos in a continuous loop (without audio). This is typically used for 39 * animated illustrations. 40 * 41 * <p>The video can be specified using {@code app:suwVideo}, specifying the raw resource to the mp4 42 * video. Optionally, {@code app:suwLoopStartMs} can be used to specify which part of the video it 43 * should loop back to 44 * 45 * <p>For optimal file size, use avconv or other video compression tool to remove the unused audio 46 * track and reduce the size of your video asset: 47 * avconv -i [input file] -vcodec h264 -crf 20 -an [output_file] 48 */ 49 @TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) 50 public class IllustrationVideoView extends TextureView implements Animatable, 51 TextureView.SurfaceTextureListener, 52 MediaPlayer.OnPreparedListener, 53 MediaPlayer.OnSeekCompleteListener, 54 MediaPlayer.OnInfoListener { 55 56 private static final String TAG = "IllustrationVideoView"; 57 58 protected float mAspectRatio = 1.0f; // initial guess until we know 59 60 @Nullable // Can be null when media player fails to initialize 61 protected MediaPlayer mMediaPlayer; 62 63 private @RawRes int mVideoResId = 0; 64 65 @VisibleForTesting Surface mSurface; 66 67 public IllustrationVideoView(Context context, AttributeSet attrs) { 68 super(context, attrs); 69 final TypedArray a = context.obtainStyledAttributes(attrs, 70 R.styleable.SuwIllustrationVideoView); 71 mVideoResId = a.getResourceId(R.styleable.SuwIllustrationVideoView_suwVideo, 0); 72 a.recycle(); 73 74 // By default the video scales without interpolation, resulting in jagged edges in the 75 // video. This works around it by making the view go through scaling, which will apply 76 // anti-aliasing effects. 77 setScaleX(0.9999999f); 78 setScaleX(0.9999999f); 79 80 setSurfaceTextureListener(this); 81 } 82 83 @Override 84 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 85 int width = MeasureSpec.getSize(widthMeasureSpec); 86 int height = MeasureSpec.getSize(heightMeasureSpec); 87 88 if (height < width * mAspectRatio) { 89 // Height constraint is tighter. Need to scale down the width to fit aspect ratio. 90 width = (int) (height / mAspectRatio); 91 } else { 92 // Width constraint is tighter. Need to scale down the height to fit aspect ratio. 93 height = (int) (width * mAspectRatio); 94 } 95 96 super.onMeasure( 97 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 98 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 99 } 100 101 /** 102 * Set the video to be played by this view. 103 * 104 * @param resId Resource ID of the video, typically an MP4 under res/raw. 105 */ 106 public void setVideoResource(@RawRes int resId) { 107 if (resId != mVideoResId) { 108 mVideoResId = resId; 109 createMediaPlayer(); 110 } 111 } 112 113 @Override 114 public void onWindowFocusChanged(boolean hasWindowFocus) { 115 super.onWindowFocusChanged(hasWindowFocus); 116 if (hasWindowFocus) { 117 start(); 118 } else { 119 stop(); 120 } 121 } 122 123 /** 124 * Creates a media player for the current URI. The media player will be started immediately if 125 * the view's window is visible. If there is an existing media player, it will be released. 126 */ 127 private void createMediaPlayer() { 128 if (mMediaPlayer != null) { 129 mMediaPlayer.release(); 130 } 131 if (mSurface == null || mVideoResId == 0) { 132 return; 133 } 134 135 mMediaPlayer = MediaPlayer.create(getContext(), mVideoResId); 136 137 if (mMediaPlayer != null) { 138 mMediaPlayer.setSurface(mSurface); 139 mMediaPlayer.setOnPreparedListener(this); 140 mMediaPlayer.setOnSeekCompleteListener(this); 141 mMediaPlayer.setOnInfoListener(this); 142 143 float aspectRatio = 144 (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth(); 145 if (mAspectRatio != aspectRatio) { 146 mAspectRatio = aspectRatio; 147 requestLayout(); 148 } 149 } else { 150 Log.wtf(TAG, "Unable to initialize media player for video view"); 151 } 152 if (getWindowVisibility() == View.VISIBLE) { 153 start(); 154 } 155 } 156 157 /** 158 * Whether the media player should play the video in a continuous loop. The default value is 159 * true. 160 */ 161 protected boolean shouldLoop() { 162 return true; 163 } 164 165 /** 166 * Release any resources used by this view. This is automatically called in 167 * onSurfaceTextureDestroyed so in most cases you don't have to call this. 168 */ 169 public void release() { 170 if (mMediaPlayer != null) { 171 mMediaPlayer.stop(); 172 mMediaPlayer.release(); 173 mMediaPlayer = null; 174 } 175 if (mSurface != null) { 176 mSurface.release(); 177 mSurface = null; 178 } 179 } 180 181 /* SurfaceTextureListener methods */ 182 183 @Override 184 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { 185 // Keep the view hidden until video starts 186 setVisibility(View.INVISIBLE); 187 mSurface = new Surface(surfaceTexture); 188 createMediaPlayer(); 189 } 190 191 @Override 192 public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { 193 } 194 195 @Override 196 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 197 release(); 198 return true; 199 } 200 201 @Override 202 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 203 } 204 205 /* Animatable methods */ 206 207 @Override 208 public void start() { 209 if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 210 mMediaPlayer.start(); 211 } 212 } 213 214 @Override 215 public void stop() { 216 if (mMediaPlayer != null) { 217 mMediaPlayer.pause(); 218 } 219 } 220 221 @Override 222 public boolean isRunning() { 223 return mMediaPlayer != null && mMediaPlayer.isPlaying(); 224 } 225 226 /* MediaPlayer callbacks */ 227 228 @Override 229 public boolean onInfo(MediaPlayer mp, int what, int extra) { 230 if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { 231 // Video available, show view now 232 setVisibility(View.VISIBLE); 233 } 234 return false; 235 } 236 237 @Override 238 public void onPrepared(MediaPlayer mp) { 239 mp.setLooping(shouldLoop()); 240 } 241 242 @Override 243 public void onSeekComplete(MediaPlayer mp) { 244 mp.start(); 245 } 246 247 public int getCurrentPosition() { 248 return mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); 249 } 250 } 251