7 package com.google.appinventor.components.runtime;
9 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
11 import android.content.Context;
12 import android.media.AudioManager;
13 import android.media.MediaPlayer;
14 import android.media.MediaPlayer.OnCompletionListener;
15 import android.media.MediaPlayer.OnErrorListener;
16 import android.media.MediaPlayer.OnPreparedListener;
17 import android.os.Bundle;
18 import android.os.Handler;
19 import android.util.Log;
20 import android.view.View;
21 import android.widget.MediaController;
22 import android.widget.VideoView;
40 import java.io.IOException;
96 version = YaVersion.VIDEOPLAYER_COMPONENT_VERSION,
97 description =
"A multimedia component capable of playing videos. "
98 +
"When the application is run, the VideoPlayer will be displayed as a "
99 +
"rectangle on-screen. If the user touches the rectangle, controls will "
100 +
"appear to play/pause, skip ahead, and skip backward within the video. "
101 +
"The application can also control behavior by calling the "
102 +
"<code>Start</code>, <code>Pause</code>, and <code>SeekTo</code> methods. "
103 +
"<p>Video files should be in "
104 +
"3GPP (.3gp) or MPEG-4 (.mp4) formats. For more details about legal "
106 +
"<a href=\"http://developer.android.com/guide/appendix/media-formats.html\""
107 +
" target=\"_blank\">Android Supported Media Formats</a>.</p>"
108 +
"<p>App Inventor for Android only permits video files under 1 MB and "
109 +
"limits the total size of an application to 5 MB, not all of which is "
110 +
"available for media (video, audio, and sound) files. If your media "
111 +
"files are too large, you may get errors when packaging or installing "
112 +
"your application, in which case you should reduce the number of media "
113 +
"files or their sizes. Most video editing software, such as Windows "
114 +
"Movie Maker and Apple iMovie, can help you decrease the size of videos "
115 +
"by shortening them or re-encoding the video into a more compact format.</p>"
116 +
"<p>You can also set the media source to a URL that points to a streaming video, "
117 +
"but the URL must point to the video file itself, not to a program that plays the video.",
118 category = ComponentCategory.MEDIA)
120 @UsesPermissions(permissionNames =
"android.permission.INTERNET")
129 private final ResizableVideoView videoView;
131 private String sourcePath;
133 private boolean inFullScreen =
false;
138 private boolean mediaReady =
false;
140 private boolean delayedStart =
false;
142 private MediaPlayer mPlayer;
144 private final Handler androidUIHandler =
new Handler();
154 videoView =
new ResizableVideoView(container.
$context());
155 videoView.setMediaController(
new MediaController(container.
$context()));
156 videoView.setOnCompletionListener(
this);
157 videoView.setOnErrorListener(
this);
158 videoView.setOnPreparedListener(
this);
161 container.
$add(
this);
169 container.
$form().setVolumeControlStream(AudioManager.STREAM_MUSIC);
193 description =
"The \"path\" to the video. Usually, this will be the "
194 +
"name of the video file, which should be added in the Designer.",
197 public
void Source(String path) {
198 final String tempPath = (path ==
null) ?
"" : path;
200 && container.$form().isDeniedPermission(READ_EXTERNAL_STORAGE)) {
203 public void HandlePermissionResponse(String permission,
boolean granted) {
207 container.$form().dispatchPermissionDeniedEvent(
VideoPlayer.this,
"Source", permission);
215 container.$form().fullScreenVideoAction(
218 sourcePath = (path ==
null) ?
"" : path;
223 videoView.invalidateMediaPlayer(
true);
226 if (videoView.isPlaying()) {
227 videoView.stopPlayback();
229 videoView.setVideoURI(
null);
230 videoView.clearAnimation();
232 if (sourcePath.length() > 0) {
233 Log.i(
"VideoPlayer",
"Source path is " + sourcePath);
239 container.$form().dispatchPermissionDeniedEvent(
this,
"Source", e);
241 }
catch (IOException e) {
242 container.$form().dispatchErrorOccurredEvent(
this,
"Source",
247 Log.i(
"VideoPlayer",
"loading video succeeded");
260 public
void Start() {
261 Log.i(
"VideoPlayer",
"Calling Start");
263 container.$form().fullScreenVideoAction(
288 description =
"Sets the volume to a number between 0 and 100. " +
289 "Values less than 0 will be treated as 0, and values greater than 100 " +
290 "will be treated as 100.")
291 public
void Volume(
int vol) {
293 vol = Math.max(vol, 0);
294 vol = Math.min(vol, 100);
295 if (mPlayer !=
null) {
296 mPlayer.setVolume(((
float) vol) / 100, ((
float) vol) / 100);
315 description =
"Pauses playback of the video. Playback can be resumed "
316 +
"at the same location by calling the <code>Start</code> method.")
317 public
void Pause() {
318 Log.i(
"VideoPlayer",
"Calling Pause");
320 container.$form().fullScreenVideoAction(
322 delayedStart =
false;
324 delayedStart =
false;
330 description =
"Resets to start of video and pauses it if video was playing.")
332 Log.i(
"VideoPlayer",
"Calling Stop");
342 description =
"Seeks to the requested time (specified in milliseconds) in the video. " +
343 "If the video is paused, the frame shown will not be updated by the seek. " +
344 "The player can jump only to key frames in the video, so seeking to times that " +
345 "differ by short intervals may not actually move to different frames.")
346 public
void SeekTo(
int ms) {
347 Log.i(
"VideoPlayer",
"Calling SeekTo");
352 container.$form().fullScreenVideoAction(
356 videoView.seekTo(ms);
361 description =
"Returns duration of the video in milliseconds.")
362 public
int GetDuration() {
363 Log.i(
"VideoPlayer",
"Calling GetDuration");
365 Bundle result = container.$form().fullScreenVideoAction(
373 return videoView.getDuration();
395 public boolean onError(MediaPlayer m,
int what,
int extra) {
404 videoView.invalidateMediaPlayer(
true);
406 delayedStart =
false;
410 "onError: what is " + what +
" 0x" + Integer.toHexString(what)
411 +
", extra is " + extra +
" 0x" + Integer.toHexString(extra));
412 container.$form().dispatchErrorOccurredEvent(
this,
"Source",
420 delayedStart =
false;
421 mPlayer = newMediaPlayer;
422 videoView.setMediaPlayer(mPlayer,
true);
428 @
SimpleEvent(description =
"The VideoPlayerError event is no longer used. "
429 +
"Please use the Screen.ErrorOccurred event instead.",
431 public
void VideoPlayerError(String message) {
448 private void prepareToDie() {
449 if (videoView.isPlaying()) {
450 videoView.stopPlayback();
452 videoView.setVideoURI(
null);
453 videoView.clearAnimation();
455 delayedStart =
false;
459 Bundle data =
new Bundle();
460 data.putBoolean(FullScreenVideoUtil.VIDEOPLAYER_FULLSCREEN,
false);
461 container.$form().fullScreenVideoAction(
462 FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_FULLSCREEN,
this, data);
475 return super.Width();
486 public
void Width(
int width) {
490 videoView.changeVideoSize(width, videoView.forcedHeight);
502 return super.Height();
513 public
void Height(
int height) {
514 super.Height(height);
517 videoView.changeVideoSize(videoView.forcedWidth, height);
539 public
void FullScreen(
boolean value) {
542 container.$form().dispatchErrorOccurredEvent(
this,
"FullScreen(true)",
547 if (value != inFullScreen) {
549 Bundle data =
new Bundle();
551 videoView.getCurrentPosition());
553 videoView.isPlaying());
557 Bundle result = container.$form().fullScreenVideoAction(
562 inFullScreen =
false;
563 container.$form().dispatchErrorOccurredEvent(
this,
"FullScreen",
567 Bundle values =
new Bundle();
569 Bundle result = container.$form().fullScreenVideoAction(
573 fullScreenKilled((Bundle) result);
576 container.$form().dispatchErrorOccurredEvent(
this,
"FullScreen",
590 inFullScreen =
false;
592 if (!newSource.equals(sourcePath)) {
595 videoView.setVisibility(View.VISIBLE);
596 videoView.requestLayout();
608 return videoView.forcedWidth;
616 return videoView.forcedHeight;
625 class ResizableVideoView
extends VideoView {
627 private MediaPlayer mVideoPlayer;
633 private Boolean mFoundMediaPlayer =
false;
639 public int forcedWidth = LENGTH_PREFERRED;
645 public int forcedHeight = LENGTH_PREFERRED;
647 public ResizableVideoView(Context context) {
651 public void onMeasure(
int specwidth,
int specheight) {
652 onMeasure(specwidth, specheight, 0);
655 private void onMeasure(
final int specwidth,
final int specheight,
final int trycount) {
665 boolean scaleHeight =
false;
666 boolean scaleWidth =
false;
667 float deviceDensity = container.$form().deviceDensity();
668 Log.i(
"VideoPlayer..onMeasure",
"Device Density = " + deviceDensity);
669 Log.i(
"VideoPlayer..onMeasure",
"AI setting dimensions as:" + forcedWidth
670 +
":" + forcedHeight);
671 Log.i(
"VideoPlayer..onMeasure",
672 "Dimenions from super>>" + MeasureSpec.getSize(specwidth) +
":"
673 + MeasureSpec.getSize(specheight));
676 int width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
677 int height = ComponentConstants.VIDEOPLAYER_PREFERRED_HEIGHT;
679 switch (forcedWidth) {
680 case LENGTH_FILL_PARENT:
681 switch (MeasureSpec.getMode(specwidth)) {
682 case MeasureSpec.EXACTLY:
683 case MeasureSpec.AT_MOST:
684 width = MeasureSpec.getSize(specwidth);
686 case MeasureSpec.UNSPECIFIED:
688 width = ((View) getParent()).getMeasuredWidth();
689 }
catch (ClassCastException cast) {
690 width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
691 }
catch (NullPointerException nullParent) {
692 width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
696 case LENGTH_PREFERRED:
697 if (mFoundMediaPlayer) {
699 width = mVideoPlayer.getVideoWidth();
700 Log.i(
"VideoPlayer.onMeasure",
"Got width from MediaPlayer>"
702 }
catch (NullPointerException nullVideoPlayer) {
704 "VideoPlayer..onMeasure",
705 "Failed to get MediaPlayer for width:\n"
706 + nullVideoPlayer.getMessage());
707 width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
717 if (forcedWidth <= LENGTH_PERCENT_TAG) {
718 int cWidth = container.$form().Width();
719 if (cWidth == 0 && trycount < 2) {
720 Log.d(
"VideoPlayer...onMeasure",
"Width not stable... trying again (onMeasure " + trycount +
")");
721 androidUIHandler.postDelayed(
new Runnable() {
724 onMeasure(specwidth, specheight, trycount + 1);
727 setMeasuredDimension(100, 100);
730 width = (int) ((
float) (cWidth * (- (width - LENGTH_PERCENT_TAG)) / 100) * deviceDensity);
731 }
else if (scaleWidth) {
732 width = (int) ((
float) width * deviceDensity);
735 switch (forcedHeight) {
736 case LENGTH_FILL_PARENT:
737 switch (MeasureSpec.getMode(specheight)) {
738 case MeasureSpec.EXACTLY:
739 case MeasureSpec.AT_MOST:
740 height = MeasureSpec.getSize(specheight);
742 case MeasureSpec.UNSPECIFIED:
748 case LENGTH_PREFERRED:
749 if (mFoundMediaPlayer) {
751 height = mVideoPlayer.getVideoHeight();
752 Log.i(
"VideoPlayer.onMeasure",
"Got height from MediaPlayer>"
754 }
catch (NullPointerException nullVideoPlayer) {
756 "VideoPlayer..onMeasure",
757 "Failed to get MediaPlayer for height:\n"
758 + nullVideoPlayer.getMessage());
759 height = ComponentConstants.VIDEOPLAYER_PREFERRED_HEIGHT;
765 height = forcedHeight;
768 if (forcedHeight <= LENGTH_PERCENT_TAG) {
769 int cHeight = container.$form().Height();
770 if (cHeight == 0 && trycount < 2) {
771 Log.d(
"VideoPlayer...onMeasure",
"Height not stable... trying again (onMeasure " + trycount +
")");
772 androidUIHandler.postDelayed(
new Runnable() {
775 onMeasure(specwidth, specheight, trycount + 1);
778 setMeasuredDimension(100, 100);
781 height = (int) ((
float) (cHeight * (- (height - LENGTH_PERCENT_TAG)) / 100) * deviceDensity);
782 }
else if (scaleHeight) {
783 height = (int) ((
float) height * deviceDensity);
788 Log.i(
"VideoPlayer.onMeasure",
"Setting dimensions to:" + width +
"x"
790 getHolder().setFixedSize(width, height);
792 setMeasuredDimension(width, height);
798 public void changeVideoSize(
int newWidth,
int newHeight) {
799 forcedWidth = newWidth;
800 forcedHeight = newHeight;
809 public void invalidateMediaPlayer(
boolean triggerRedraw) {
810 mFoundMediaPlayer =
false;
820 setMediaPlayer(MediaPlayer newMediaPlayer,
boolean triggerRedraw) {
821 mVideoPlayer = newMediaPlayer;
822 mFoundMediaPlayer =
true;