Commit 98fd7ce8 authored by Vance's avatar Vance Committed by Jeffrey I. Schiller

Add Resizable and Full Screen Video Player

Change-Id: Ica112991b56c830fcd42eb436b3921f63ce81723
parent cba736dc
......@@ -77,10 +77,6 @@ public final class MockVideoPlayer extends MockVisibleComponent {
@Override
protected boolean isPropertyVisible(String propertyName) {
if (propertyName.equals(PROPERTY_NAME_WIDTH) ||
propertyName.equals(PROPERTY_NAME_HEIGHT)) {
return false;
}
return super.isPropertyVisible(propertyName);
}
}
......@@ -753,6 +753,13 @@ public final class YoungAndroidFormUpgrader {
// No properties need to be modified to upgrade to version 3.
srcCompVersion = 3;
}
if (srcCompVersion < 4) {
// The VideoPlayer.height and VideoPlayer.width getter and setters were marked as
// visible to the user.
// The FullScreen property was created.
// No properties need to be modified to upgrade to version 4.
srcCompVersion = 4;
}
return srcCompVersion;
}
......
......@@ -951,6 +951,11 @@ public class BlockSaveFile {
}
blkCompVersion = 3;
}
if (blkCompVersion < 4) {
// The VideoPlayer.height and VideoPlayer.width getter and setters were marked as
// visible to the user
blkCompVersion = 4;
}
return blkCompVersion;
}
......
......@@ -176,11 +176,12 @@ public class YaVersion {
// - PLAYER_COMPONENT_VERSION was incremented to 4.
// For YOUNG_ANDROID_VERSION 58:
// - FORM_COMPONENT_VERSION was incremented to 7.
// For YOUNG_ANDROID_VERSION 59:
//The Camcorder component was added.
// For YOUNG_ANDROID_VERION 59:
// The Camcorder component was added.
// For YOUNG_ANDROID_VERION 60:
// - VIDEOPLAYER_COMPONENT_VERSION was incremented to 4.
public static final int YOUNG_ANDROID_VERSION = 59;
public static final int YOUNG_ANDROID_VERSION = 60;
// ............................... Blocks Language Version Number ...............................
......@@ -499,7 +500,10 @@ public class YaVersion {
// - The VideoPlayer.VideoPlayerError event was added.
// For VIDEOPLAYER_COMPONENT_VERSION 3:
// - The VideoPlayer.VideoPlayerError event was marked userVisible false and is no longer used.
public static final int VIDEOPLAYER_COMPONENT_VERSION = 3;
// For VIDEOPLAYER_COMPONENT_VERSION 4:
// - The VideoPlayer.width and VideoPlayer.height variables were marked as user visible.
// - The FullScreen property was added to the VideoPlayer.
public static final int VIDEOPLAYER_COMPONENT_VERSION = 4;
public static final int VOTING_COMPONENT_VERSION = 1;
......
......@@ -6,7 +6,6 @@ import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
......@@ -18,6 +17,7 @@ import com.google.appinventor.components.runtime.collect.Lists;
import com.google.appinventor.components.runtime.collect.Maps;
import com.google.appinventor.components.runtime.collect.Sets;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.FullScreenVideoUtil;
import com.google.appinventor.components.runtime.util.JsonUtil;
import com.google.appinventor.components.runtime.util.MediaUtil;
import com.google.appinventor.components.runtime.util.SdkLevel;
......@@ -25,6 +25,7 @@ import com.google.appinventor.components.runtime.util.ViewUtil;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
......@@ -131,6 +132,8 @@ public class Form extends Activity
// event.
private String nextFormName;
private FullScreenVideoUtil fullScreenVideoUtil;
@Override
public void onCreate(Bundle icicle) {
// Called when the activity is first created
......@@ -155,6 +158,8 @@ public class Form extends Activity
startupValue = startIntent.getStringExtra(ARGUMENT_NAME);
}
fullScreenVideoUtil = new FullScreenVideoUtil(this, androidUIHandler);
// Add application components to the form
$define();
......@@ -350,6 +355,25 @@ public class Form extends Activity
onDestroyListeners.add(component);
}
public Dialog onCreateDialog(int id, Bundle args) {
switch(id) {
case FullScreenVideoUtil.FULLSCREEN_VIDEO_DIALOG_FLAG:
return fullScreenVideoUtil.createFullScreenVideoDialog(args);
default:
return super.onCreateDialog(id, args);
}
}
public void onPrepareDialog(int id, Dialog dialog, Bundle args) {
switch(id) {
case FullScreenVideoUtil.FULLSCREEN_VIDEO_DIALOG_FLAG:
fullScreenVideoUtil.prepareFullScreenVideoDialog(dialog, args);
break;
default:
super.onPrepareDialog(id, dialog, args);
}
}
/**
* Compiler-generated method to initialize and add application components to
* the form. We just provide an implementation here to artificially make
......@@ -494,6 +518,7 @@ public class Form extends Activity
if (backgroundDrawable != null) {
ViewUtil.setBackgroundImage(frameLayout, backgroundDrawable);
}
setContentView(frameLayout);
frameLayout.requestLayout();
}
......@@ -1117,4 +1142,41 @@ public class Form extends Activity
throw e.getTargetException();
}
}
/**
* Perform some action related to fullscreen video display.
* @param action
* Can be any of the following:
* <ul>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_DURATION}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_FULLSCREEN}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_PAUSE}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_PLAY}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_SEEK}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_SOURCE}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_STOP}
* </li>
* </ul>
* @param source
* The VideoPlayer to use in some actions.
* @param data
* Used by the method. This object varies depending on the action.
* @return Varies depending on what action was passed in.
*/
public synchronized Bundle fullScreenVideoAction(int action, VideoPlayer source, Object data) {
return fullScreenVideoUtil.performAction(action, source, data);
}
}
......@@ -15,12 +15,16 @@ import com.google.appinventor.components.common.ComponentConstants;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.FullScreenVideoUtil;
import com.google.appinventor.components.runtime.util.MediaUtil;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.MediaController;
......@@ -50,46 +54,64 @@ import java.io.IOException;
* things are working solidly.
*/
/**
* TODO: The resizing of the VideoPlayer at runtime does not work well on some
* devices such as the Motorola Droid. The behavior is almost random when resizing
* the VideoPlayer on such devices. When App Inventor includes the features to
* restrict certain devices, the VideoPlayer should be updated.
*/
/**
* Implementation of VideoPlayer, using {@link android.widget.VideoView}.
*
* @author halabelson@google.com (Hal Abelson)
*/
@DesignerComponent(version = YaVersion.VIDEOPLAYER_COMPONENT_VERSION,
description = "A multimedia component capable of playing videos. " +
"When the application is run, the VideoPlayer will be displayed as a " +
"rectangle on-screen. If the user touches the rectangle, controls will " +
"appear to play/pause, skip ahead, and skip backward within the video. " +
"The application can also control behavior by calling the " +
"<code>Start</code>, <code>Pause</code>, and <code>SeekTo</code> methods. " +
"<p>Video files should be in Windows Media Video (.wmv) format, " +
"3GPP (.3gp), or MPEG-4 (.mp4). For more details about legal " +
"formats, see " +
"<a href=\"http://developer.android.com/guide/appendix/media-formats.html\"" +
" target=\"_blank\">Android Supported Media Formats</a>.</p>" +
"<p>App Inventor for Android only permits video files under 1 MB and " +
"limits the total size of an application to 5 MB, not all of which is " +
"available for media (video, audio, and sound) files. If your media " +
"files are too large, you may get errors when packaging or installing " +
"your application, in which case you should reduce the number of media " +
"files or their sizes. Most video editing software, such as Windows " +
"Movie Maker and Apple iMovie, can help you decrease the size of videos " +
"by shortening them or re-encoding the video into a more compact format.</p>",
@DesignerComponent(
version = YaVersion.VIDEOPLAYER_COMPONENT_VERSION,
description = "A multimedia component capable of playing videos. "
+ "When the application is run, the VideoPlayer will be displayed as a "
+ "rectangle on-screen. If the user touches the rectangle, controls will "
+ "appear to play/pause, skip ahead, and skip backward within the video. "
+ "The application can also control behavior by calling the "
+ "<code>Start</code>, <code>Pause</code>, and <code>SeekTo</code> methods. "
+ "<p>Video files should be in Windows Media Video (.wmv) format, "
+ "3GPP (.3gp), or MPEG-4 (.mp4). For more details about legal "
+ "formats, see "
+ "<a href=\"http://developer.android.com/guide/appendix/media-formats.html\""
+ " target=\"_blank\">Android Supported Media Formats</a>.</p>"
+ "<p>App Inventor for Android only permits video files under 1 MB and "
+ "limits the total size of an application to 5 MB, not all of which is "
+ "available for media (video, audio, and sound) files. If your media "
+ "files are too large, you may get errors when packaging or installing "
+ "your application, in which case you should reduce the number of media "
+ "files or their sizes. Most video editing software, such as Windows "
+ "Movie Maker and Apple iMovie, can help you decrease the size of videos "
+ "by shortening them or re-encoding the video into a more compact format.</p>",
category = ComponentCategory.MEDIA)
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.INTERNET")
public final class VideoPlayer extends AndroidViewComponent
implements OnDestroyListener, Deleteable, OnCompletionListener, OnErrorListener {
public final class VideoPlayer extends AndroidViewComponent implements
OnDestroyListener, Deleteable, OnCompletionListener, OnErrorListener,
OnPreparedListener {
/*
* Video clip with player controls (touch it to activate)
*/
private final VideoView videoView;
private final ResizableVideoView videoView;
private String sourcePath; // name of media source
private boolean inFullScreen = false;
// The VideoView does not always start playing if Start is called
// shortly after the source is set. These flags are used to fix this
// problem.
private boolean mediaReady = false;
private boolean delayedStart = false;
/**
* Creates a new VideoPlayer component.
*
......@@ -98,16 +120,19 @@ public final class VideoPlayer extends AndroidViewComponent
public VideoPlayer(ComponentContainer container) {
super(container);
container.$form().registerForOnDestroy(this);
videoView = new VideoView(container.$context());
videoView = new ResizableVideoView(container.$context());
videoView.setMediaController(new MediaController(container.$context()));
videoView.setOnCompletionListener(this);
videoView.setOnErrorListener(this);
videoView.setOnPreparedListener(this);
// add the component to the designated container
container.$add(this);
// set a default size
container.setChildWidth(this, ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH);
container.setChildHeight(this, ComponentConstants.VIDEOPLAYER_PREFERRED_HEIGHT);
container.setChildWidth(this,
ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH);
container.setChildHeight(this,
ComponentConstants.VIDEOPLAYER_PREFERRED_HEIGHT);
// Make volume buttons control media, not ringer.
container.$form().setVolumeControlStream(AudioManager.STREAM_MUSIC);
......@@ -123,20 +148,31 @@ public final class VideoPlayer extends AndroidViewComponent
/**
* Sets the video source.
*
* <p/>See {@link MediaUtil#determineMediaSource} for information about what
* a path can be.
* <p/>
* See {@link MediaUtil#determineMediaSource} for information about what a
* path can be.
*
* @param path the path to the video source
* @param path
* the path to the video source
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_ASSET,
defaultValue = "")
@SimpleProperty(
description = "The \"path\" to the video. Usually, this will be the " +
"name of the video file, which should be added in the Designer.",
description = "The \"path\" to the video. Usually, this will be the "
+ "name of the video file, which should be added in the Designer.",
category = PropertyCategory.BEHAVIOR)
public void Source(String path) {
if (inFullScreen) {
container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_SOURCE, this, path);
} else {
sourcePath = (path == null) ? "" : path;
// The source may change for the MediaPlayer, and
// getVideoWidth or getVideoHeight may be called
// creating an error in ResizableVideoView.
videoView.invalidateMediaPlayer(true);
// Clear the previous video.
if (videoView.isPlaying()) {
videoView.stopPlayback();
......@@ -148,6 +184,7 @@ public final class VideoPlayer extends AndroidViewComponent
Log.i("VideoPlayer", "Source path is " + sourcePath);
try {
mediaReady = false;
MediaUtil.loadVideoView(videoView, container.$form(), sourcePath);
} catch (IOException e) {
container.$form().dispatchErrorOccurredEvent(this, "Source",
......@@ -158,26 +195,51 @@ public final class VideoPlayer extends AndroidViewComponent
Log.i("VideoPlayer", "loading video succeeded");
}
}
}
/**
* Plays the media specified by the source. These won't normally be used in
* the most elementary applications, because videoView brings up its own
* player controls when the video is touched.
*/
@SimpleFunction(
description = "Starts playback of the video.")
@SimpleFunction(description = "Starts playback of the video.")
public void Start() {
Log.i("VideoPlayer", "Calling Start");
if (inFullScreen) {
container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_PLAY, this, null);
} else {
if (mediaReady) {
videoView.start();
} else {
delayedStart = true;
}
}
}
/**
* Method for starting the VideoPlayer once the media has been loaded.
* Not visible to users.
*/
public void delayedStart() {
delayedStart = true;
Start();
}
@SimpleFunction(
description = "Pauses playback of the video. Playback can be resumed " +
"at the same location by calling the <code>Start</code> method.")
description = "Pauses playback of the video. Playback can be resumed "
+ "at the same location by calling the <code>Start</code> method.")
public void Pause() {
Log.i("VideoPlayer", "Calling Pause");
if (inFullScreen) {
container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_PAUSE, this, null);
delayedStart = false;
} else {
delayedStart = false;
videoView.pause();
}
}
@SimpleFunction(
description = "Seeks to the requested time (specified in milliseconds) in the video. " +
......@@ -187,16 +249,31 @@ public final class VideoPlayer extends AndroidViewComponent
if (ms < 0) {
ms = 0;
}
if (inFullScreen) {
container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_SEEK, this, ms);
} else {
// There is no harm if the milliseconds is longer than the duration.
videoView.seekTo(ms);
}
}
@SimpleFunction(
description = "Returns duration of the video in milliseconds.")
public int GetDuration() {
Log.i("VideoPlayer", "Calling GetDuration");
if (inFullScreen) {
Bundle result = container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_DURATION, this, null);
if (result.getBoolean(FullScreenVideoUtil.ACTION_SUCESS)) {
return result.getInt(FullScreenVideoUtil.ACTION_DATA);
} else {
return 0;
}
} else {
return videoView.getDuration();
}
}
// OnCompletionListener implementation
......@@ -217,15 +294,39 @@ public final class VideoPlayer extends AndroidViewComponent
@Override
public boolean onError(MediaPlayer m, int what, int extra) {
Log.e("VideoPlayer", "onError: what is " + what + " 0x" + Integer.toHexString(what) +
", extra is " + extra + " 0x" + Integer.toHexString(extra));
// The ResizableVideoView onMeasure method attempts to use the MediaPlayer
// to measure
// the VideoPlayer; but in the event of an error, the MediaPlayer
// may report dimensions of zero video width and height.
// Since VideoPlayer currently (7/10/2012) sets its size always
// to some non-zero number, the MediaPlayer is invalidated here
// to prevent onMeasure from setting width and height as zero.
videoView.invalidateMediaPlayer(true);
delayedStart = false;
mediaReady = false;
Log.e("VideoPlayer",
"onError: what is " + what + " 0x" + Integer.toHexString(what)
+ ", extra is " + extra + " 0x" + Integer.toHexString(extra));
container.$form().dispatchErrorOccurredEvent(this, "Source",
ErrorMessages.ERROR_UNABLE_TO_LOAD_MEDIA, sourcePath);
return true;
}
@SimpleEvent(description = "The VideoPlayerError event is no longer used. " +
"Please use the Screen.ErrorOccurred event instead.",
@Override
public void onPrepared(MediaPlayer newMediaPlayer) {
mediaReady = true;
delayedStart = false;
videoView.setMediaPlayer(newMediaPlayer, true);
if (delayedStart) {
Start();
}
}
@SimpleEvent(description = "The VideoPlayerError event is no longer used. "
+ "Please use the Screen.ErrorOccurred event instead.",
userVisible = false)
public void VideoPlayerError(String message) {
}
......@@ -250,21 +351,26 @@ public final class VideoPlayer extends AndroidViewComponent
}
videoView.setVideoURI(null);
videoView.clearAnimation();
}
/*
* Unfortunately, to prevent the user from setting the width and height
* of the component, we have to also prevent them from getting the width
* and height of the component.
*/
delayedStart = false;
mediaReady = false;
if (inFullScreen) {
Bundle data = new Bundle();
data.putBoolean(FullScreenVideoUtil.VIDEOPLAYER_FULLSCREEN, false);
container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_FULLSCREEN, this, data);
}
}
/**
* Returns the component's horizontal width, measured in pixels.
*
* @return width in pixels
*/
@Override
@SimpleProperty(userVisible = false)
@SimpleProperty
public int Width() {
return super.Width();
}
......@@ -274,10 +380,14 @@ public final class VideoPlayer extends AndroidViewComponent
*
* @param width in pixels
*/
@Override
@SimpleProperty(userVisible = false)
@SimpleProperty(userVisible = true)
public void Width(int width) {
super.Width(width);
// Forces a layout of the ResizableVideoView
videoView.changeVideoSize(width, videoView.forcedHeight);
}
/**
......@@ -285,8 +395,9 @@ public final class VideoPlayer extends AndroidViewComponent
*
* @return height in pixels
*/
@Override
@SimpleProperty(userVisible = false)
@SimpleProperty
public int Height() {
return super.Height();
}
......@@ -294,11 +405,274 @@ public final class VideoPlayer extends AndroidViewComponent
/**
* Specifies the component's vertical height, measured in pixels.
*
* @param height in pixels
* @param height
* in pixels
*/
@Override
@SimpleProperty(userVisible = false)
@SimpleProperty(userVisible = true)
public void Height(int height) {
super.Height(height);
// Forces a layout of the ResizableVideoView
videoView.changeVideoSize(videoView.forcedWidth, height);
}
/**
* Returns whether the VideoPlayer's video is currently being
* shown in fullscreen mode or not.
* @return True if video is being shown in fullscreen. False otherwise.
*/
@SimpleProperty
public boolean FullScreen() {
return inFullScreen;
}
/**
* Sets whether the video should be shown in fullscreen or not.
*
* @param value If True, the video will be shown in fullscreen.
* If False and {@link VideoPlayer#FullScreen()} returns True, fullscreen
* mode will be exited. If False and {@link VideoPlayer#FullScreen()}
* returns False, nothing occurs.
*/
@SimpleProperty(userVisible = true)
public void FullScreen(boolean value) {
if (value != inFullScreen) {
if (value) {
Bundle data = new Bundle();
data.putInt(FullScreenVideoUtil.VIDEOPLAYER_POSITION,
videoView.getCurrentPosition());
data.putBoolean(FullScreenVideoUtil.VIDEOPLAYER_PLAYING,
videoView.isPlaying());
videoView.pause();
data.putBoolean(FullScreenVideoUtil.VIDEOPLAYER_FULLSCREEN, true);
data.putString(FullScreenVideoUtil.VIDEOPLAYER_SOURCE, sourcePath);
Bundle result = container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_FULLSCREEN, this, data);
if (result.getBoolean(FullScreenVideoUtil.ACTION_SUCESS)) {
inFullScreen = true;
} else {
inFullScreen = false;
container.$form().dispatchErrorOccurredEvent(this, "FullScreen",
ErrorMessages.ERROR_VIDEOPLAYER_FULLSCREEN_UNAVAILBLE, "");
}
} else {
Bundle values = new Bundle();
values.putBoolean(FullScreenVideoUtil.VIDEOPLAYER_FULLSCREEN, false);
Bundle result = container.$form().fullScreenVideoAction(
FullScreenVideoUtil.FULLSCREEN_VIDEO_ACTION_FULLSCREEN, this,
values);
if (result.getBoolean(FullScreenVideoUtil.ACTION_SUCESS)) {
fullScreenKilled((Bundle) result);
} else {
inFullScreen = true;
container.$form().dispatchErrorOccurredEvent(this, "FullScreen",
ErrorMessages.ERROR_VIDEOPLAYER_FULLSCREEN_CANT_EXIT, "");
}
}
}
}
/**
* Notify this VideoPlayer that its video is no longer being shown
* in fullscreen.
* @param data See {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil}
* for an example of what data should contain.
*/
public void fullScreenKilled(Bundle data) {
inFullScreen = false;
String newSource = data.getString(FullScreenVideoUtil.VIDEOPLAYER_SOURCE);
if (!newSource.equals(sourcePath)) {
Source(newSource);
}
videoView.setVisibility(View.VISIBLE);
videoView.requestLayout();
SeekTo(data.getInt(FullScreenVideoUtil.VIDEOPLAYER_POSITION));
if (data.getBoolean(FullScreenVideoUtil.VIDEOPLAYER_PLAYING)) {
Start();
}
}
/**
* Get the value passed in {@link VideoPlayer#Width(int)}
* @return The width value.
*/
public int getPassedWidth() {
return videoView.forcedWidth;
}
/**
* Get the value passed in {@link VideoPlayer#Height(int)}
* @return The height value.
*/
public int getPassedHeight() {
return videoView.forcedHeight;
}
/**
* Extends VideoView to allow resizing of the view that ignores the aspect
* ratio of the video being played.
*
* @author Vance Turnewitsch
*/
class ResizableVideoView extends VideoView {
private MediaPlayer mVideoPlayer;
/*
* Used by onMeasure to determine whether the mVideoPlayer should be used to
* measure the view.
*/
private Boolean mFoundMediaPlayer = false;
/**
* Used by onMeasure to determine what type of size the VideoPlayer should
* be.
*/
public int forcedWidth = LENGTH_PREFERRED;
/**
* Used by onMeasure to determine what type of size the VideoPlayer should
* be.
*/
public int forcedHeight = LENGTH_PREFERRED;
public ResizableVideoView(Context context) {
super(context);
}
public void onMeasure(int specwidth, int specheight) {
// Since super.onMeasure uses the aspect ratio of the video being
// played, it is not called.
// http://grepcode.com/file/repository.grepcode.com/java/ext/
// com.google.android/android/2.2.2_r1/android/widget/VideoView.java
// #VideoView.onMeasure%28int%2Cint%29
// Log messages in this method are not commented out for testing the
// changes
// on other devices.
Log.i("VideoPlayer..onMeasure", "AI setting dimensions as:" + forcedWidth
+ ":" + forcedHeight);
Log.i("VideoPlayer..onMeasure",
"Dimenions from super>>" + MeasureSpec.getSize(specwidth) + ":"
+ MeasureSpec.getSize(specheight));
// The VideoPlayer's dimensions must always be some non-zero number.
int width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
int height = ComponentConstants.VIDEOPLAYER_PREFERRED_HEIGHT;
switch (forcedWidth) {
case LENGTH_FILL_PARENT:
switch (MeasureSpec.getMode(specwidth)) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
width = MeasureSpec.getSize(specwidth);
break;
case MeasureSpec.UNSPECIFIED:
try {
width = ((View) getParent()).getMeasuredWidth();
} catch (ClassCastException cast) {
width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
} catch (NullPointerException nullParent) {
width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
}
}
break;
case LENGTH_PREFERRED:
if (mFoundMediaPlayer) {
try {
width = mVideoPlayer.getVideoWidth();
Log.i("VideoPlayer.onMeasure", "Got width from MediaPlayer>"
+ width);
} catch (NullPointerException nullVideoPlayer) {
Log.e(
"VideoPlayer..onMeasure",
"Failed to get MediaPlayer for width:\n"
+ nullVideoPlayer.getMessage());
width = ComponentConstants.VIDEOPLAYER_PREFERRED_WIDTH;
}
} else {
}
break;
default:
width = forcedWidth;
}
switch (forcedHeight) {
case LENGTH_FILL_PARENT:
switch (MeasureSpec.getMode(specheight)) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
height = MeasureSpec.getSize(specheight);
break;
case MeasureSpec.UNSPECIFIED:
// Use height from ComponentConstants
// The current measuring of components ignores FILL_PARENT for height,
// and does not actually fill the height of the parent container.
}
break;
case LENGTH_PREFERRED:
if (mFoundMediaPlayer) {
try {
height = mVideoPlayer.getVideoHeight();
Log.i("VideoPlayer.onMeasure", "Got height from MediaPlayer>"
+ height);
} catch (NullPointerException nullVideoPlayer) {
Log.e(
"VideoPlayer..onMeasure",
"Failed to get MediaPlayer for height:\n"
+ nullVideoPlayer.getMessage());
height = ComponentConstants.VIDEOPLAYER_PREFERRED_HEIGHT;
}
}
break;
default:
height = forcedHeight;
}
// Forces the video playing in the VideoView to scale.
// Some Android devices though will not scale the video playing.
Log.i("VideoPlayer.onMeasure", "Setting dimensions to:" + width + "x"
+ height);
getHolder().setFixedSize(width, height);
setMeasuredDimension(width, height);
}
/**
* Resize the view size and request a layout.
*/
public void changeVideoSize(int newWidth, int newHeight) {
forcedWidth = newWidth;
forcedHeight = newHeight;
forceLayout();
invalidate();
}
/*
* Used to keep onMeasure from using the mVideoPlayer in measuring.
*/
public void invalidateMediaPlayer(boolean triggerRedraw) {
mFoundMediaPlayer = false;
mVideoPlayer = null;
if (triggerRedraw) {
forceLayout();
invalidate();
}
}
public void
setMediaPlayer(MediaPlayer newMediaPlayer, boolean triggerRedraw) {
mVideoPlayer = newMediaPlayer;
mFoundMediaPlayer = true;
if (triggerRedraw) {
forceLayout();
invalidate();
}
}
}
}
// Copyright MIT
package com.google.appinventor.components.runtime.util;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.MediaController;
/**
* A modified MediaController that allows adding to a
* {@link android.view.ViewGroup} instead of the Window that Android adds the
* MediaController to.
*
* This class manages displaying the controller GUI.
*
* @author Vance Turnewitsch
*
*/
public class CustomMediaController extends MediaController implements
View.OnTouchListener {
// The view whose touch events are listened to.
private View mAnchorView;
/*
* How long the GUI should be shown when the anchorView passed in
*/
private int mShowTime = 3000;
public CustomMediaController(Context context) {
super(context);
}
/**
* Sets the visibility of the GUI to {@link android.view.View#VISIBLE}
* and calls {@link android.widget.MediaController#show(int)}.
* @param timeout
* How long the GUI should be shown for.
*/
@Override
public void show(int timeout) {
setVisibility(VISIBLE);
super.show(timeout);
}
/**
* Sets the visibility of the GUI to {@link android.view.View#VISIBLE}
* and calls {@link android.widget.MediaController#show()}.
*/
@Override
public void show() {
setVisibility(VISIBLE);
super.show();
}
/**
* Attempts to remove this CustomMediaController from the Window that Android
* automatically adds it to and add it to another
* {@link android.view.ViewGroup}.
*
* @param parent
* The {@link android.view.ViewGroup} to add the CustomMediaController
* to.
* @param params
* The {@link android.view.ViewGroup.LayoutParams} to use when adding
* the CustomMediaController to the parent.
*/
public boolean addTo(ViewGroup parent, ViewGroup.LayoutParams params) {
Object mParent = getParent();
if (mParent != null && mParent instanceof ViewGroup) {
((ViewGroup) mParent).removeView(this);
parent.addView(this, params);
return true;
} else {
Log.e("CustomMediaController.addTo",
"MediaController not available in fullscreen.");
return false;
}
}
/**
* Calls {@link android.widget.MediaController#setAnchorView(View)} and sets
* up a listener that shows a GUI when the anchorView is touched.
*/
@Override
public void setAnchorView(View anchorView) {
mAnchorView = anchorView;
mAnchorView.setOnTouchListener(this);
super.setAnchorView(anchorView);
}
/**
* Calls {@link android.widget.MediaController#hide()} and sets the visibility
* of this object to {@link android.view.View#INVISIBLE}.
*/
@Override
public void hide() {
super.hide();
setVisibility(INVISIBLE);
}
/**
* Called when the anchorView passed in
* {@link CustomMediaController#setAnchorView(View)} is touched. Shows the
* controller GUI.
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v == mAnchorView) {
show(mShowTime);
}
return false;
}
}
......@@ -115,8 +115,10 @@ public final class ErrorMessages {
public static final int ERROR_PHONE_UNSUPPORTED_SEARCH_IN_CONTACT_PICKING = 1108;
//Camcorder errors
public static final int ERROR_CAMCORDER_NO_CLIP_RETURNED = 1201;
// Please start the next group of error numbers at 1301.
// VideoPlayer errors
public static final int ERROR_VIDEOPLAYER_FULLSCREEN_UNAVAILBLE = 1301;
public static final int ERROR_VIDEOPLAYER_FULLSCREEN_CANT_EXIT = 1302;
//Please start the next group of error numbers at 1401.
// Mapping of error numbers to error message format strings.
private static final Map<Integer, String> errorMessages;
......@@ -315,6 +317,11 @@ public final class ErrorMessages {
// Camcorder errors
errorMessages.put(ERROR_CAMCORDER_NO_CLIP_RETURNED,
"The camcorder did not return a clip.");
// VideoPlayer errors
errorMessages.put(ERROR_VIDEOPLAYER_FULLSCREEN_UNAVAILBLE,
"Cannot start fullscreen mode.");
errorMessages.put(ERROR_VIDEOPLAYER_FULLSCREEN_CANT_EXIT,
"Cannot exit fullscreen mode.");
}
private ErrorMessages() {
......
// Copyright MIT
package com.google.appinventor.components.runtime.util;
import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.VideoPlayer;
import android.R;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnShowListener;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;
import android.widget.VideoView;
import java.io.IOException;
/**
* Used by the {@link com.google.appinventor.components.runtime.Form} class to
* display videos in fullscreen.
*
* @author Vance Turnewitsch
*/
public class FullScreenVideoUtil implements OnCompletionListener,
OnPreparedListener, OnShowListener {
// Constants
public static final int FULLSCREEN_VIDEO_DIALOG_FLAG = 189;
public static final int FULLSCREEN_VIDEO_ACTION_SEEK = 190;
public static final int FULLSCREEN_VIDEO_ACTION_PLAY = 191;
public static final int FULLSCREEN_VIDEO_ACTION_PAUSE = 192;
public static final int FULLSCREEN_VIDEO_ACTION_STOP = 193;
public static final int FULLSCREEN_VIDEO_ACTION_SOURCE = 194;
public static final int FULLSCREEN_VIDEO_ACTION_FULLSCREEN = 195;
public static final int FULLSCREEN_VIDEO_ACTION_DURATION = 196;
public static final String VIDEOPLAYER_FULLSCREEN = "FullScreenKey";
public static final String VIDEOPLAYER_PLAYING = "PlayingKey";
public static final String VIDEOPLAYER_POSITION = "PositionKey";
public static final String VIDEOPLAYER_SOURCE = "SourceKey";
public static final String ACTION_SUCESS = "ActionSuccess";
public static final String ACTION_DATA = "ActionData";
// The Dialog and other components used in the Dialog for displaying the
// video.
private Dialog mFullScreenVideoDialog;
private FrameLayout mFullScreenVideoHolder;
private VideoView mFullScreenVideoView;
private CustomMediaController mFullScreenVideoController;
private FrameLayout.LayoutParams mMediaControllerParams = new FrameLayout.LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM);
private Form mForm;
// The player whose video is currently being shown.
private VideoPlayer mFullScreenPlayer = null;
// The data passed in by the player that requested
// fullscreen display of its video.
private Bundle mFullScreenVideoBundle;
// Used for showing a preview of a paused video.
private Handler mHandler;
/**
* @param form
* The {@link com.google.appinventor.components.runtime.Form} that
* this FullScreenVideoUtil will use.
* @param handler
* A {@link android.os.Handler} created on the UI thread. Used for
* displaying a preview of a paused video.
*/
public FullScreenVideoUtil(Form form, Handler handler) {
mForm = form;
mHandler = handler;
mFullScreenVideoDialog = new Dialog(mForm,
R.style.Theme_NoTitleBar_Fullscreen) {
public void onBackPressed() {
// Allows the user to force exiting full-screen.
Bundle values = new Bundle();
values.putInt(VIDEOPLAYER_POSITION,
mFullScreenVideoView.getCurrentPosition());
values.putBoolean(VIDEOPLAYER_PLAYING,
mFullScreenVideoView.isPlaying());
values.putString(VIDEOPLAYER_SOURCE,
mFullScreenVideoBundle
.getString(VIDEOPLAYER_SOURCE));
mFullScreenPlayer.fullScreenKilled(values);
super.onBackPressed();
}
};
}
/**
* Perform some action and get a result. The data to pass in and the result
* returned are controlled by what the action is.
*
* @param action
* Can be any of the following:
* <ul>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_DURATION}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_FULLSCREEN}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_PAUSE}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_PLAY}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_SEEK}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_SOURCE}
* </li>
* <li>
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_STOP}
* </li>
* </ul>
* @param source
* The VideoPlayer to use in some actions.
* @param data
* Used by the method. This object varies depending on the action.
* @return Varies depending on what action was passed in.
*/
public synchronized Bundle performAction(int action, VideoPlayer source,
Object data) {
Log.i("Form.fullScreenVideoAction", "Actions:" + action + " Source:"
+ source + ": Current Source:" + mFullScreenPlayer + " Data:" + data);
Bundle result = new Bundle();
result.putBoolean(ACTION_SUCESS, true);
if (source == mFullScreenPlayer) {
switch (action) {
case FULLSCREEN_VIDEO_ACTION_FULLSCREEN:
return doFullScreenVideoAction(source, (Bundle) data);
case FULLSCREEN_VIDEO_ACTION_PAUSE:
if (showing()) {
mFullScreenVideoView.pause();
return result;
}
result.putBoolean(ACTION_SUCESS, false);
return result;
case FULLSCREEN_VIDEO_ACTION_PLAY:
if (showing()) {
mFullScreenVideoView.start();
return result;
}
result.putBoolean(ACTION_SUCESS, false);
return result;
case FULLSCREEN_VIDEO_ACTION_SEEK:
if (showing()) {
mFullScreenVideoView.seekTo((Integer) data);
return result;
}
result.putBoolean(ACTION_SUCESS, false);
return result;
case FULLSCREEN_VIDEO_ACTION_STOP:
if (showing()) {
mFullScreenVideoView.stopPlayback();
return result;
}
result.putBoolean(ACTION_SUCESS, false);
return result;
case FULLSCREEN_VIDEO_ACTION_SOURCE:
if (showing()) {
result.putBoolean(ACTION_SUCESS,setSource((String) data, true));
return result;
}
result.putBoolean(ACTION_SUCESS, false);
return result;
case FULLSCREEN_VIDEO_ACTION_DURATION:
if (showing()) {
result.putInt(ACTION_DATA, mFullScreenVideoView.getDuration());
return result;
}
result.putBoolean(ACTION_SUCESS, false);
return result;
}
} else if (action == FULLSCREEN_VIDEO_ACTION_FULLSCREEN) {
// There may be a dialog already being shown.
if (showing() && mFullScreenPlayer != null) {
Bundle values = new Bundle();
values.putInt(VIDEOPLAYER_POSITION,
mFullScreenVideoView.getCurrentPosition());
values.putBoolean(VIDEOPLAYER_PLAYING,
mFullScreenVideoView.isPlaying());
values.putString(VIDEOPLAYER_SOURCE,
mFullScreenVideoBundle
.getString(VIDEOPLAYER_SOURCE));
mFullScreenPlayer.fullScreenKilled(values);
}
return doFullScreenVideoAction(source, (Bundle) data);
}
// This should never be called.
result.putBoolean(ACTION_SUCESS, false);
return result;
}
/*
* Displays or hides a full-screen video.
*/
private Bundle doFullScreenVideoAction(VideoPlayer source, Bundle data) {
Log.i("Form.doFullScreenVideoAction", "Source:" + source + " Data:" + data);
Bundle result = new Bundle();
result.putBoolean(ACTION_SUCESS, true);
if (data.getBoolean(VIDEOPLAYER_FULLSCREEN) == true) {
mFullScreenPlayer = source;
mFullScreenVideoBundle = data;
if (!mFullScreenVideoDialog.isShowing()) {
mForm.showDialog(FULLSCREEN_VIDEO_DIALOG_FLAG,
(Bundle) data);
return result;
} else {
mFullScreenVideoView.pause();
result.putBoolean(ACTION_SUCESS, setSource(
mFullScreenVideoBundle.getString(VIDEOPLAYER_SOURCE),false));
return result;
}
} else {
if (showing()) {
result.putBoolean(VIDEOPLAYER_PLAYING,
mFullScreenVideoView.isPlaying());
result.putInt(VIDEOPLAYER_POSITION,
mFullScreenVideoView.getCurrentPosition());
result.putString(VIDEOPLAYER_SOURCE,
mFullScreenVideoBundle
.getString(VIDEOPLAYER_SOURCE));
mFullScreenPlayer = null;
mFullScreenVideoBundle = null;
mForm.dismissDialog(FULLSCREEN_VIDEO_DIALOG_FLAG);
return result;
}
}
result.putBoolean(ACTION_SUCESS, false);
return result;
}
/**
* Creates the dialog for displaying a fullscreen VideoView.
*
* @param data
* The data to be used by this class in displaying the fullscreen
* video.
* @return The created Dialog
*/
public Dialog createFullScreenVideoDialog(final Bundle data) {
mFullScreenVideoBundle = data;
mFullScreenVideoView = new VideoView(mForm);
mFullScreenVideoHolder = new FrameLayout(mForm);
mFullScreenVideoController = new CustomMediaController(mForm);
mFullScreenVideoView.setId(mFullScreenVideoView.hashCode());
mFullScreenVideoHolder.setId(mFullScreenVideoHolder.hashCode());
mFullScreenVideoView.setMediaController(mFullScreenVideoController);
mFullScreenVideoView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
Log.i("FullScreenVideoUtil..onTouch", "Video Touched!!");
return false;
}
});
mFullScreenVideoController.setAnchorView(mFullScreenVideoView);
String orientation = mForm.ScreenOrientation();
if (orientation.equals("landscape")
|| orientation.equals("sensorLandscape")
|| orientation.equals("reverseLandscape")) {
mFullScreenVideoView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.FILL_PARENT, Gravity.CENTER));
} else {
mFullScreenVideoView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.FILL_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
}
mFullScreenVideoHolder
.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT));
mFullScreenVideoHolder.addView(mFullScreenVideoView);
// Add the MediaController to the Dialog
mFullScreenVideoController.addTo(mFullScreenVideoHolder,
mMediaControllerParams);
mFullScreenVideoDialog.setContentView(mFullScreenVideoHolder);
return mFullScreenVideoDialog;
}
/**
* Call just before displaying a fullscreen video Dialog. This method
* sets up some listeners.
*
* @param dia
* The dialog that will display the video.
* @param data
*/
public void prepareFullScreenVideoDialog(Dialog dia, Bundle data) {
dia.setOnShowListener(this);
mFullScreenVideoView.setOnPreparedListener(this);
mFullScreenVideoView.setOnCompletionListener(this);
}
/**
* @return True if the internal Dialog has been created. False otherwise.
*/
public boolean dialogInitialized() {
return mFullScreenVideoDialog != null;
}
/**
* @return True if {@link FullScreenVideoUtil#dialogInitialized()} is true and
* the Dialog is showing. False otherwise.
*/
public boolean showing() {
return dialogInitialized() && mFullScreenVideoDialog.isShowing();
}
/**
* Sets the source to be used by the fullscreen video Dialog. This method
* also attempts to load the internal VideoView with the source.
*
* @param source
* The source path to use. The {@link MediaUtil} is used to load the
* source.
* @param clearSeek
* If True, the video will start playing at position zero. If False,
* the video will start playing from the
* {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#VIDEOPLAYER_POSITION}
* value of the Bundle passed in the
* {@link FullScreenVideoUtil#performAction(int, VideoPlayer, Object)}
* or {@link FullScreenVideoUtil#createFullScreenVideoDialog(Bundle)}
* @return True if the video was successfully loaded. False otherwise.
*/
public boolean setSource(String source, boolean clearSeek) {
try {
if (clearSeek) {
mFullScreenVideoBundle.putInt(VIDEOPLAYER_POSITION,
0);
}
MediaUtil.loadVideoView(mFullScreenVideoView, mForm, (String) source);
mFullScreenVideoBundle.putString(VIDEOPLAYER_SOURCE,
source);
return true;
} catch (IOException e) {
mForm.dispatchErrorOccurredEvent(mFullScreenPlayer, "Source",
ErrorMessages.ERROR_UNABLE_TO_LOAD_MEDIA, source);
return false;
}
}
/**
* Called when the video has finished playing.
*/
@Override
public void onCompletion(MediaPlayer arg0) {
if (mFullScreenPlayer != null) {
mFullScreenPlayer.Completed();
}
}
/**
* Called when the video has been loaded.
*/
@Override
public void onPrepared(MediaPlayer arg0) {
Log.i(
"FullScreenVideoUtil..onPrepared",
"Seeking to:"
+ mFullScreenVideoBundle
.getInt(VIDEOPLAYER_POSITION));
mFullScreenVideoView.seekTo(mFullScreenVideoBundle
.getInt(VIDEOPLAYER_POSITION));
if (mFullScreenVideoBundle
.getBoolean(VIDEOPLAYER_PLAYING)) {
mFullScreenVideoView.start();
} else {
mFullScreenVideoView.start();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mFullScreenVideoView.pause();
}
}, 100);
}
}
/**
* Called when the Dialog is about to be shown.
*/
@Override
public void onShow(DialogInterface arg0) {
try {
MediaUtil.loadVideoView(mFullScreenVideoView, mForm,
mFullScreenVideoBundle
.getString(VIDEOPLAYER_SOURCE));
} catch (IOException e) {
mForm.dispatchErrorOccurredEvent(mFullScreenPlayer, "Source",
ErrorMessages.ERROR_UNABLE_TO_LOAD_MEDIA, mFullScreenVideoBundle
.getString(VIDEOPLAYER_SOURCE));
return;
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment