• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.google.android.setupdesign.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.media.MediaPlayer.OnErrorListener;
26 import android.media.MediaPlayer.OnInfoListener;
27 import android.media.MediaPlayer.OnPreparedListener;
28 import android.media.MediaPlayer.OnSeekCompleteListener;
29 import android.net.Uri;
30 import android.os.Build.VERSION_CODES;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.view.Surface;
34 import android.view.TextureView;
35 import android.view.TextureView.SurfaceTextureListener;
36 import android.view.View;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.RawRes;
39 import androidx.annotation.VisibleForTesting;
40 import com.google.android.setupcompat.util.BuildCompatUtils;
41 import com.google.android.setupdesign.R;
42 import java.io.IOException;
43 
44 /**
45  * A view for displaying videos in a continuous loop (without audio). This is typically used for
46  * animated illustrations.
47  *
48  * <p>The video can be specified using {@code app:sudVideo}, specifying the raw resource to the mp4
49  * video. Optionally, {@code app:sudLoopStartMs} can be used to specify which part of the video it
50  * should loop back to
51  *
52  * <p>For optimal file size, use avconv or other video compression tool to remove the unused audio
53  * track and reduce the size of your video asset: avconv -i [input file] -vcodec h264 -crf 20 -an
54  * [output_file]
55  */
56 @TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH)
57 public class IllustrationVideoView extends TextureView
58     implements Animatable,
59         SurfaceTextureListener,
60         OnPreparedListener,
61         OnSeekCompleteListener,
62         OnInfoListener,
63         OnErrorListener {
64 
65   private static final String TAG = "IllustrationVideoView";
66 
67   private float aspectRatio = 1.0f; // initial guess until we know
68 
69   @Nullable // Can be null when media player fails to initialize
70   protected MediaPlayer mediaPlayer;
71 
72   private @RawRes int videoResId = 0;
73 
74   private String videoResPackageName;
75 
76   @VisibleForTesting Surface surface;
77 
78   private boolean prepared;
79 
80   private boolean shouldPauseVideoWhenFinished = true;
81 
82   /**
83    * The visibility of this view as set by the user. This view combines this with {@link
84    * #isMediaPlayerLoading} to determine the final visibility.
85    */
86   private int visibility = View.VISIBLE;
87 
88   /**
89    * Whether the media player is loading. This is used to hide this view to avoid a flash with a
90    * color different from the background while the media player is trying to render the first frame.
91    * Note: if this TextureView is not visible, it will never load the surface texture, and never
92    * play the video.
93    */
94   private boolean isMediaPlayerLoading = false;
95 
IllustrationVideoView(Context context, AttributeSet attrs)96   public IllustrationVideoView(Context context, AttributeSet attrs) {
97     super(context, attrs);
98     if (!isInEditMode()) {
99       init(context, attrs);
100     }
101   }
102 
init(Context context, AttributeSet attrs)103   private void init(Context context, AttributeSet attrs) {
104     final TypedArray a =
105         context.obtainStyledAttributes(attrs, R.styleable.SudIllustrationVideoView);
106     final int videoResId = a.getResourceId(R.styleable.SudIllustrationVideoView_sudVideo, 0);
107 
108     // TODO: remove the usage of BuildCompatUtils#isAtLeatestS if VERSION_CODE.S is
109     // support by system.
110     if (BuildCompatUtils.isAtLeastS()) {
111       boolean shouldPauseVideo =
112           a.getBoolean(R.styleable.SudIllustrationVideoView_sudPauseVideoWhenFinished, true);
113       setPauseVideoWhenFinished(shouldPauseVideo);
114     }
115 
116     a.recycle();
117     setVideoResource(videoResId);
118 
119     // By default the video scales without interpolation, resulting in jagged edges in the
120     // video. This works around it by making the view go through scaling, which will apply
121     // anti-aliasing effects.
122     setScaleX(0.9999999f);
123     setScaleX(0.9999999f);
124 
125     setSurfaceTextureListener(this);
126   }
127 
128   @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)129   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
130     int width = MeasureSpec.getSize(widthMeasureSpec);
131     int height = MeasureSpec.getSize(heightMeasureSpec);
132 
133     if (height < width * aspectRatio) {
134       // Height constraint is tighter. Need to scale down the width to fit aspect ratio.
135       width = (int) (height / aspectRatio);
136     } else {
137       // Width constraint is tighter. Need to scale down the height to fit aspect ratio.
138       height = (int) (width * aspectRatio);
139     }
140 
141     super.onMeasure(
142         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
143         MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
144   }
145 
146   /**
147    * Set the video and video package name to be played by this view.
148    *
149    * @param videoResId Resource ID of the video, typically an MP4 under res/raw.
150    * @param videoResPackageName The package name of videoResId.
151    */
setVideoResource(@awRes int videoResId, String videoResPackageName)152   public void setVideoResource(@RawRes int videoResId, String videoResPackageName) {
153     if (videoResId != this.videoResId
154         || (videoResPackageName != null && !videoResPackageName.equals(this.videoResPackageName))) {
155       this.videoResId = videoResId;
156       this.videoResPackageName = videoResPackageName;
157       createMediaPlayer();
158     }
159   }
160 
161   /**
162    * Set the video to be played by this view.
163    *
164    * @param resourceEntry the {@link com.google.android.setupdesign.util.Partner.ResourceEntry} of
165    *     the video, typically an MP4 under res/raw.
166    */
setVideoResourceEntry( com.google.android.setupdesign.util.Partner.ResourceEntry resourceEntry)167   public void setVideoResourceEntry(
168       com.google.android.setupdesign.util.Partner.ResourceEntry resourceEntry) {
169     setVideoResource(resourceEntry.id, resourceEntry.packageName);
170   }
171 
172   /**
173    * Set the video to be played by this view.
174    *
175    * @param resourceEntry the {@link com.google.android.setupcompat.partnerconfig.ResourceEntry} of
176    *     the video, typically an MP4 under res/raw.
177    */
setVideoResourceEntry( com.google.android.setupcompat.partnerconfig.ResourceEntry resourceEntry)178   public void setVideoResourceEntry(
179       com.google.android.setupcompat.partnerconfig.ResourceEntry resourceEntry) {
180     setVideoResource(resourceEntry.getResourceId(), resourceEntry.getPackageName());
181   }
182 
183   /**
184    * Set the video to be played by this view.
185    *
186    * @param resId Resource ID of the video, typically an MP4 under res/raw.
187    */
setVideoResource(@awRes int resId)188   public void setVideoResource(@RawRes int resId) {
189     setVideoResource(resId, getContext().getPackageName());
190   }
191 
192   /**
193    * Sets whether the video pauses during the screen transition.
194    *
195    * @param paused Whether the video pauses.
196    */
setPauseVideoWhenFinished(boolean paused)197   public void setPauseVideoWhenFinished(boolean paused) {
198     shouldPauseVideoWhenFinished = paused;
199   }
200 
201   @Override
onWindowFocusChanged(boolean hasWindowFocus)202   public void onWindowFocusChanged(boolean hasWindowFocus) {
203     super.onWindowFocusChanged(hasWindowFocus);
204     if (hasWindowFocus) {
205       start();
206     } else {
207       stop();
208     }
209   }
210 
211   /**
212    * Creates a media player for the current URI. The media player will be started immediately if the
213    * view's window is visible. If there is an existing media player, it will be released.
214    */
createMediaPlayer()215   protected void createMediaPlayer() {
216     if (mediaPlayer != null) {
217       mediaPlayer.release();
218     }
219     if (surface == null || videoResId == 0) {
220       return;
221     }
222 
223     mediaPlayer = new MediaPlayer();
224 
225     mediaPlayer.setSurface(surface);
226     mediaPlayer.setOnPreparedListener(this);
227     mediaPlayer.setOnSeekCompleteListener(this);
228     mediaPlayer.setOnInfoListener(this);
229     mediaPlayer.setOnErrorListener(this);
230 
231     setVideoResourceInternal(videoResId, videoResPackageName);
232   }
233 
setVideoResourceInternal(@awRes int videoRes, String videoResPackageName)234   private void setVideoResourceInternal(@RawRes int videoRes, String videoResPackageName) {
235     Uri uri = Uri.parse("android.resource://" + videoResPackageName + "/" + videoRes);
236     try {
237       mediaPlayer.setDataSource(getContext(), uri, null);
238       mediaPlayer.prepareAsync();
239     } catch (IOException e) {
240       Log.e(TAG, "Unable to set video data source: " + videoRes, e);
241     }
242   }
243 
createSurface()244   protected void createSurface() {
245     if (surface != null) {
246       surface.release();
247       surface = null;
248     }
249     // Reattach only if it has been previously released
250     SurfaceTexture surfaceTexture = getSurfaceTexture();
251     if (surfaceTexture != null) {
252       setIsMediaPlayerLoading(true);
253       surface = new Surface(surfaceTexture);
254     }
255   }
256 
257   @Override
onWindowVisibilityChanged(int visibility)258   protected void onWindowVisibilityChanged(int visibility) {
259     super.onWindowVisibilityChanged(visibility);
260     if (visibility == View.VISIBLE) {
261       reattach();
262     } else {
263       release();
264     }
265   }
266 
267   @Override
setVisibility(int visibility)268   public void setVisibility(int visibility) {
269     this.visibility = visibility;
270     if (isMediaPlayerLoading && visibility == View.VISIBLE) {
271       visibility = View.INVISIBLE;
272     }
273     super.setVisibility(visibility);
274   }
275 
setIsMediaPlayerLoading(boolean isMediaPlayerLoading)276   private void setIsMediaPlayerLoading(boolean isMediaPlayerLoading) {
277     this.isMediaPlayerLoading = isMediaPlayerLoading;
278     setVisibility(this.visibility);
279   }
280 
281   /**
282    * Whether the media player should play the video in a continuous loop. The default value is true.
283    */
shouldLoop()284   protected boolean shouldLoop() {
285     return true;
286   }
287 
288   /**
289    * Release any resources used by this view. This is automatically called in
290    * onSurfaceTextureDestroyed so in most cases you don't have to call this.
291    */
release()292   public void release() {
293     if (mediaPlayer != null) {
294       mediaPlayer.release();
295       mediaPlayer = null;
296       prepared = false;
297     }
298     if (surface != null) {
299       surface.release();
300       surface = null;
301     }
302   }
303 
reattach()304   private void reattach() {
305     if (surface == null) {
306       initVideo();
307     }
308   }
309 
initVideo()310   private void initVideo() {
311     if (getWindowVisibility() != View.VISIBLE) {
312       return;
313     }
314     createSurface();
315     if (surface != null) {
316       createMediaPlayer();
317     } else {
318       // This can happen if this view hasn't been drawn yet
319       Log.i(TAG, "Surface is null");
320     }
321   }
322 
onRenderingStart()323   protected void onRenderingStart() {}
324 
325   /* SurfaceTextureListener methods */
326 
327   @Override
onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height)328   public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
329     setIsMediaPlayerLoading(true);
330     initVideo();
331   }
332 
333   @Override
onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height)334   public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {}
335 
336   @Override
onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture)337   public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
338     release();
339     return true;
340   }
341 
342   @Override
onSurfaceTextureUpdated(SurfaceTexture surfaceTexture)343   public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {}
344 
345   /* Animatable methods */
346 
347   @Override
start()348   public void start() {
349     if (prepared && mediaPlayer != null && !mediaPlayer.isPlaying()) {
350       mediaPlayer.start();
351     }
352   }
353 
354   @Override
stop()355   public void stop() {
356     if (shouldPauseVideoWhenFinished) {
357       if (prepared && mediaPlayer != null) {
358         mediaPlayer.pause();
359       }
360     } else {
361       // do not pause the media player.
362     }
363   }
364 
365   @Override
isRunning()366   public boolean isRunning() {
367     return mediaPlayer != null && mediaPlayer.isPlaying();
368   }
369 
370   /* MediaPlayer callbacks */
371 
372   @Override
onInfo(MediaPlayer mp, int what, int extra)373   public boolean onInfo(MediaPlayer mp, int what, int extra) {
374     if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
375       setIsMediaPlayerLoading(false);
376       onRenderingStart();
377     }
378     return false;
379   }
380 
381   @Override
onPrepared(MediaPlayer mp)382   public void onPrepared(MediaPlayer mp) {
383     prepared = true;
384     mp.setLooping(shouldLoop());
385 
386     float aspectRatio = 0.0f;
387     if (mp.getVideoWidth() > 0 && mp.getVideoHeight() > 0) {
388       aspectRatio = (float) mp.getVideoHeight() / mp.getVideoWidth();
389     } else {
390       Log.w(TAG, "Unexpected video size=" + mp.getVideoWidth() + "x" + mp.getVideoHeight());
391     }
392     if (Float.compare(this.aspectRatio, aspectRatio) != 0) {
393       this.aspectRatio = aspectRatio;
394       requestLayout();
395     }
396     if (getWindowVisibility() == View.VISIBLE) {
397       start();
398     }
399   }
400 
401   @Override
onSeekComplete(MediaPlayer mp)402   public void onSeekComplete(MediaPlayer mp) {
403     if (isPrepared()) {
404       mp.start();
405     } else {
406       Log.e(TAG, "Seek complete but media player not prepared");
407     }
408   }
409 
getCurrentPosition()410   public int getCurrentPosition() {
411     return mediaPlayer == null ? 0 : mediaPlayer.getCurrentPosition();
412   }
413 
isPrepared()414   protected boolean isPrepared() {
415     return prepared;
416   }
417 
418   @Override
onError(MediaPlayer mediaPlayer, int what, int extra)419   public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
420     Log.w(TAG, "MediaPlayer error. what=" + what + " extra=" + extra);
421     return false;
422   }
423 
424   /**
425    * Seeks to specified time position.
426    *
427    * @param milliseconds the offset in milliseconds from the start to seek to
428    * @throws IllegalStateException if the internal player engine has not been initialized
429    */
seekTo(int milliseconds)430   public void seekTo(int milliseconds) {
431     if (mediaPlayer != null) {
432       mediaPlayer.seekTo(milliseconds);
433     }
434   }
435 
436   @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
getMediaPlayer()437   public MediaPlayer getMediaPlayer() {
438     return mediaPlayer;
439   }
440 
getAspectRatio()441   protected float getAspectRatio() {
442     return aspectRatio;
443   }
444 }
445