Custom video controls

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.

How to do it...

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:

  1. First, when using a 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;
      }
    }
  2. Then, in the 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);
  3. Next, we pass the instance of ISurfaceHolderCallback to the AddCallback() method:
    holder.AddCallback(this);
  4. Before we have a playing surface, we can start setting up our MediaPlayer instance that will play the video:
    mediaPlayer = new MediaPlayer();
    mediaPlayer.SetDataSource(this, videoUri);
    mediaPlayer.SetAudioStreamType(Stream.Music);
  5. When the surface is created, we pass it to the media player using SetDisplay:
    mediaPlayer.SetDisplay(surfaceHolder);
  6. We will also want to subscribe to the VideoSizeChanged event, which will provide us with the video size:
    mediaPlayer.VideoSizeChanged += (sender, e) => {
      surfaceHolder.SetFixedSize(e.Width, e.Height);
    };
  7. To begin playing, we prepare the player asynchronously and then start the playback:
    mediaPlayer.Prepared += (sender, e) => {
      mediaPlayer.Start();
    };
    mediaPlayer.PrepareAsync();
  8. Finally, we need to ensure that when the activity is paused, we release the resources used by the media player:
    if (mediaPlayer != null) {
      mediaPlayer.Release();
      mediaPlayer = null;
    }
  9. If the surface is destroyed or created while the video is playing, we can assign the new surface, or 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:

  1. First, we will need a 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;
      }
    });
  2. We will attach logic that will show the controls when the surface is tapped, and add a message in the Handler instance to hide them after a few seconds:
    const int HideTimeout = 5000;
    surfaceView.Click += delegate {
      videoController.Visibility = ViewStates.Visible;
      handler.SendEmptyMessageDelayed(
        WhatHideControls, HideTimeout);
    };
  3. Then, when the surface is tapped again, it should hide the controls, ensuring that it removes any existing messages in the Handler instance:
    handler.RemoveMessages(WhatHideControls); 
    videoController.Visibility = ViewStates.Gone; 
  4. Before we show the controls, we need to ensure that we update the controls to match the media player's state:
    if (mediaPlayer.IsPlaying) {
      playPauseButton.SetImageResource(
        Android.Resource.Drawable.IcMediaPause);
    }
    else {
      playPauseButton.SetImageResource(
        Android.Resource.Drawable.IcMediaPlay);
    }
  5. If we want to update the progress in the controls, we can add a new message to the 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;
      }
    }
  6. Then, when the controls are going to be shown, we start updating the progress by adding the update message to the Handler instance:
    progressBar.Max = mediaPlayer.Duration;
    handler.SendEmptyMessage(WhatProgress);

How it works...

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.

Note

Using a MediaPlayer instance to play a video is the same as when playing audio, except that the display surface needs to be set using the SetDisplay() method.

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.

Tip

The VideoSizeChanged event is used to obtain the dimensions of the video being played.

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.

Tip

A surface is not created on demand, but rather constructed when a SurfaceView instance becomes visible.

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.

Tip

When the media player is provided a null display, the video will not be visible, but the audio will continue playing.

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.

Note

A Handler type can be used to queue messages to be actioned by another thread either immediately or after a delay.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset