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