The VideoView
is great for displaying videos with a basic set of controls; however, we may require more flexibility and control over how the video is played.
We can play a video onto a SurfaceView
using a MediaPlayer
instance if we need fine control over what we can do or if we want custom video controls:
SurfaceView
instance, we need to implement the ISurfaceHolderCallback
interface, ensuring that we keep track of the surface:public class MainActivity : Activity, ISurfaceHolderCallback { private ISurfaceHolder surfaceHolder; public void SurfaceChanged( ISurfaceHolder holder, Format format, int width, int height) { } public void SurfaceCreated(ISurfaceHolder holder) { surfaceHolder = holder; } public void SurfaceDestroyed(ISurfaceHolder holder) { surfaceHolder = null; } }
OnCreate()
method, we want to get hold of the ISurfaceHolder
instance using the Holder
property of the SurfaceView
instance:var holder = surfaceView.Holder; holder.SetType(SurfaceType.PushBuffers);
ISurfaceHolderCallback
to the AddCallback()
method:holder.AddCallback(this);
MediaPlayer
instance that will play the video:mediaPlayer = new MediaPlayer(); mediaPlayer.SetDataSource(this, videoUri); mediaPlayer.SetAudioStreamType(Stream.Music);
SetDisplay
:mediaPlayer.SetDisplay(surfaceHolder);
VideoSizeChanged
event, which will provide us with the video size:mediaPlayer.VideoSizeChanged += (sender, e) => { surfaceHolder.SetFixedSize(e.Width, e.Height); };
mediaPlayer.Prepared += (sender, e) => { mediaPlayer.Start(); }; mediaPlayer.PrepareAsync();
if (mediaPlayer != null) { mediaPlayer.Release(); mediaPlayer = null; }
null
surface, to the player:if (mediaPlayer != null) { mediaPlayer.SetDisplay(surfaceHolder); }
Once our video is playing in the surface view, we can add the video controls. Here, we will toggle visibility when the surface is tapped:
Handler
instance that will do the work of managing the visibility of the video controls:const int WhatHideControls = 1; handler = new Handler(message => { if (message.What == WhatHideControls) { videoController.Visibility = ViewStates.Gone; } });
Handler
instance to hide them after a few seconds:const int HideTimeout = 5000; surfaceView.Click += delegate { videoController.Visibility = ViewStates.Visible; handler.SendEmptyMessageDelayed( WhatHideControls, HideTimeout); };
Handler
instance:handler.RemoveMessages(WhatHideControls); videoController.Visibility = ViewStates.Gone;
if (mediaPlayer.IsPlaying) { playPauseButton.SetImageResource( Android.Resource.Drawable.IcMediaPause); } else { playPauseButton.SetImageResource( Android.Resource.Drawable.IcMediaPlay); }
Handler
instance that fires every second:const int WhatProgress = 2; if (message.What == WhatProgress) { if (mediaPlayer != null && mediaPlayer.IsPlaying) { var pos = mediaPlayer.CurrentPosition; var next = 1000 - (pos % 1000); progressBar.Progress = pos; handler.SendEmptyMessageDelayed(WhatProgress, next); } else { progressBar.Progress = 0; } }
Handler
instance:progressBar.Max = mediaPlayer.Duration; handler.SendEmptyMessage(WhatProgress);
We can play a video in a VideoView
instance, but this is fairly limited in what we can do. We can create a custom media controller, but it is still limited by the small API exposed by the VideoView
instance. If we want to create a more advanced player, we will have to create it from scratch.
This is not as difficult as it may initially appear to be, as there are only three main parts to the view. There is the underlying MediaPlayer
instance that will render a video on a SurfaceView
instance and be controlled by a set of buttons or other controls.
In order to play a video, we will need an instance of MediaPlayer
. We work with this in a very similar manner to playing an audio. The only difference is that as the player will be rendering a video, we need to let it know about the surface it can render onto. We do this by passing the surface to the SetDisplay()
or SetSurface()
method. As the video size may not be the same as the surface size, we can use the VideoSizeChanged
event to be notified of when the video size is known or changed. Other than those two extra members, we set up the data source and audio stream as we would for a normal audio file.
To actually display the video, we will have to set up and use a SurfaceView
instance. A SurfaceView
instance is a special kind of view that provides a dedicated drawing surface to another object. The actual drawing is done using the SurfaceView
instance's ISurfaceHolder
, which is obtained from the Holder
property. We do not directly control the creation and destruction of a surface, but rather listen for an event that lets us know that the surface is ready for drawing.
To start using the surface, we need to obtain the ISurfaceHolder
implementation from the SurfaceView
instance, using the Holder
property. Then, we pass an implementation of the ISurfaceHolderCallback
interface to the AddCallback()
method of the holder. On devices running Android versions below 3.0, we must also specify the type of surface using the SetType()
method. For videos, we just use the SurfaceType.PushBuffers
value.
When the surface is ready, the SurfaceCreated()
callback method is invoked with a reference to a valid surface holder instance. We keep a reference to this surface holder and use it to display the video. At some point, such as when the app is paused, the surface may be destroyed. This will result in the SurfaceDestroyed()
callback method being invoked. When this happens, the surface is no longer available for drawing and we should update the media player's display surface.
Once we receive the surface, we can pass it on to the media player using the SetDisplay()
method. Doing this will result in the player rendering the video in the view. If the surface is destroyed, we can remove the surface from the player by passing null
to SetDisplay
. This will cause the video to stop, but the audio will continue playing. To stop the audio, we will have to stop or pause the player.
If we are going to create a custom video player, we will need to provide the user with a means to control the playback. To do this, we can create a view, which contains the various controls that will overlay the video. One option to show the controls is to wait for the user to tap the video, after which the controls will be shown. After a few seconds, we can hide the controls and only show the video.
To update the controls with the video progress or buffer options, we need to query the media player each time. There are no events on the player, so we will have to check every second or more. An easy way to do this is to make use of a Handler
type, which is a queue that allows us the scheduling of the messages to be handled on a particular thread. We can use this mechanism to schedule a message to update the various controls every second.
Using a handler is very simple and all we have to do is create an instance of the Handler
type. The constructor allows us to pass in a delegate that will be executed when a message needs to be handled. Messages can be handled immediately after it is sent to the handler, or after some delay. When the delegate is executed, we are provided with the Message
instance that was queued. We can use this message to perform any action based on the data in the message. One of the most important pieces of information in a Message
instance is the What
data, which is a custom ID that we use to identify what the message is about.
We can enqueue a message in the Handler
instance either with an empty message or with a populated message. To send an empty message, we can use the SendEmptyMessage()
method and pass in the What
value. To post a more complex message, we can first create one using the Obtain()
method on the Handler
. The result is a Message
instance that we populate and then enqueue using the SendMessage()
method.
If we want to enqueue a message that should be dequeued at a point in the later future, we can send a delayed message. This is the same as sending a normal message, but we use the SendEmptyMessageDelayed()
and SendMessageDelayed()
methods instead. These methods take an additional parameter—the delay in milliseconds before the message is actually dequeued.
To remove a message, regardless of how it was enqueued, we can pass the What
value to the RemoveMessages()
method. This will dequeue all the messages with the specified What
value, but will not execute the Handler
delegate.