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