Hi there
I have a video player in Xamarin forms that works for IOS and android. Before IOS 16 the video controls, play, pause, full screen etc all appeared as expected.
After the device has been updated to IOS 16 the controls are now missing (This is both on a real device and emulator.) It seems the latest IOS update has had some breaking changes as they have updated the video controls.
I have found some similar topics and issues around.
https://developer.apple.com/forums/thread/711360
https://stackoverflow.com/questions/72830620/playback-buttons-do-not-appear-in-an-avplayer-after-adding-a-layer-ios-16
These suggest that I may need to add the view controller to the parent UIView.
I am unsure how to do this in xamarin as I don't have a "controller" above.
Here is the code for the video player
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using ScreenConfiguration.FormsVideoLibrary;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Input;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(VideoPlayer),
typeof(ScreenConfiguration.iOS.FormsVideoLibrary.VideoPlayerRenderer))]
namespace ScreenConfiguration.iOS.FormsVideoLibrary
{
public class VideoPlayerRenderer : ViewRenderer<VideoPlayer, UIView>
{
AVPlayer player;
AVPlayerItem playerItem;
AVPlayerViewController _playerViewController; // solely for ViewController property
public override UIViewController ViewController => _playerViewController;
protected override void OnElementChanged(ElementChangedEventArgs<VideoPlayer> args)
{
base.OnElementChanged(args);
if (args.NewElement != null)
{
if (Control == null)
{
// Create AVPlayerViewController
_playerViewController = new AVPlayerViewController();
// Set Player property to AVPlayer
player = new AVPlayer();
_playerViewController.Player = player;
if (args.NewElement.Fit)
{
_playerViewController.VideoGravity = AVLayerVideoGravity.Resize;
}
else
{
_playerViewController.VideoGravity = AVLayerVideoGravity.ResizeAspect;
}
if (args.NewElement.AreTransportControlsEnabled)
{
_playerViewController.ShowsPlaybackControls = false;
}
else
{
_playerViewController.ShowsPlaybackControls = true;
}
var x = _playerViewController.View;
SetNativeControl(x);
}
SetAreTransportControlsEnabled();
SetSource();
args.NewElement.UpdateStatus += OnUpdateStatus;
args.NewElement.PlayRequested += OnPlayRequested;
args.NewElement.PauseRequested += OnPauseRequested;
args.NewElement.StopRequested += OnStopRequested;
args.NewElement.VideoStepForwardEvent += OnVideoStepForwardEvent;
args.NewElement.VideoStepBackwardEvent += OnVideoStepBackwardEvent;
}
if (args.OldElement != null)
{
args.OldElement.UpdateStatus -= OnUpdateStatus;
args.OldElement.PlayRequested -= OnPlayRequested;
args.OldElement.PauseRequested -= OnPauseRequested;
args.OldElement.StopRequested -= OnStopRequested;
args.OldElement.VideoStepForwardEvent -= OnVideoStepForwardEvent;
args.OldElement.VideoStepBackwardEvent -= OnVideoStepBackwardEvent;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (player != null)
{
player.ReplaceCurrentItemWithPlayerItem(null);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(sender, args);
if (args.PropertyName == VideoPlayer.AreTransportControlsEnabledProperty.PropertyName)
{
SetAreTransportControlsEnabled();
}
else if (args.PropertyName == VideoPlayer.SourceProperty.PropertyName)
{
SetSource();
}
else if (args.PropertyName == VideoPlayer.PositionProperty.PropertyName)
{
TimeSpan controlPosition = ConvertTime(player.CurrentTime);
if (Math.Abs((controlPosition - Element.Position).TotalSeconds) > 1)
{
player.Seek(CMTime.FromSeconds(Element.Position.TotalSeconds, 1));
}
}
}
void SetAreTransportControlsEnabled()
{
((AVPlayerViewController)ViewController).ShowsPlaybackControls = Element.AreTransportControlsEnabled;
}
void SetSource()
{
AVAsset asset = null;
if (Element.Source is UriVideoSource)
{
string uri = (Element.Source as UriVideoSource).Uri;
if (!String.IsNullOrWhiteSpace(uri))
{
try
{
asset = AVAsset.FromUrl(new NSUrl(uri));
}
catch(Exception ex)
{
}
}
}
else if (Element.Source is FileVideoSource)
{
string uri = (Element.Source as FileVideoSource).File;
if (!String.IsNullOrWhiteSpace(uri))
{
asset = AVAsset.FromUrl(new NSUrl(uri));
}
}
else if (Element.Source is ResourceVideoSource)
{
string path = (Element.Source as ResourceVideoSource).Path;
if (!String.IsNullOrWhiteSpace(path))
{
string directory = Path.GetDirectoryName(path);
string filename = Path.GetFileNameWithoutExtension(path);
string extension = Path.GetExtension(path).Substring(1);
NSUrl url = NSBundle.MainBundle.GetUrlForResource(filename, extension, directory);
asset = AVAsset.FromUrl(url);
}
}
if (asset != null)
{
playerItem = new AVPlayerItem(asset);
var videoWidth = asset.NaturalSize.Width;
var videoHeight = asset.NaturalSize.Height;
}
else
{
playerItem = null;
}
if (playerItem != null)
{
player.ReplaceCurrentItemWithPlayerItem(playerItem);
player.CurrentItem.PreferredForwardBufferDuration = 1;
}
if (playerItem != null && Element.AutoPlay)
{
player.Play();
}
}
// Event handler to update status
void OnUpdateStatus(object sender, EventArgs args)
{
VideoStatus videoStatus = VideoStatus.NotReady;
switch (player.Status)
{
case AVPlayerStatus.ReadyToPlay:
switch (player.TimeControlStatus)
{
case AVPlayerTimeControlStatus.Playing:
videoStatus = VideoStatus.Playing;
break;
case AVPlayerTimeControlStatus.Paused:
videoStatus = VideoStatus.Paused;
break;
}
break;
}
((IVideoPlayerController)Element).Status = videoStatus;
if (playerItem != null)
{
((IVideoPlayerController)Element).Duration = ConvertTime(playerItem.Duration);
((IElementController)Element).SetValueFromRenderer(VideoPlayer.PositionProperty, ConvertTime(playerItem.CurrentTime));
}
}
TimeSpan ConvertTime(CMTime cmTime)
{
return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
}
// Event handlers to implement methods
void OnPlayRequested(object sender, EventArgs args)
{
player.Play();
}
void OnPauseRequested(object sender, EventArgs args)
{
player.Pause();
}
void OnStopRequested(object sender, EventArgs args)
{
player.Pause();
player.Seek(new CMTime(0, 1));
}
void OnVideoStepForwardEvent(object sender, EventArgs args)
{
TimeSpan controlPosition = ConvertTime(player.CurrentTime);
TimeSpan controlDuration = ConvertTime(playerItem.Duration);
if (controlPosition.TotalMilliseconds + 100 < controlDuration.TotalMilliseconds)
{
double calculatedSeconds = (controlPosition.TotalMilliseconds + 100)/1000;//(double)((controlPosition.TotalMilliseconds + 100) / 1000);
player.Seek(CMTime.FromSeconds(calculatedSeconds, 60000), CMTime.Zero, CMTime.Zero);
}
else
{
TimeSpan calculatedSeconds = ConvertTime(playerItem.Duration);
player.Seek(CMTime.FromSeconds((calculatedSeconds.TotalMilliseconds * 1000), 60000));
}
}
void OnVideoStepBackwardEvent(object sender, EventArgs args)
{
TimeSpan controlPosition = ConvertTime(player.CurrentTime);
TimeSpan controlDuration = ConvertTime(playerItem.Duration);
if (controlPosition.TotalMilliseconds - 100 > 0)
{
double calculatedSeconds = (controlPosition.TotalMilliseconds - 100) / 1000;
player.Seek(CMTime.FromSeconds(calculatedSeconds, 60000),CMTime.Zero, CMTime.Zero);
}
else
{
player.Seek(CMTime.FromSeconds(0, 1));
}
}
ICommand OnTappedCmd
{
get
{
return new Command(OnTapped);
}
}
void OnTapped()
{
Element.VideoSelected = true;
}
}
}
In the page I just add it to a grid with a url.
<ContentPage.Content>
<Grid BackgroundColor="Black">
<formsVideoLibrary:VideoPlayer x:Name="VideoPlayerControl"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
IsVisible="True"
Source="{Binding VideoPlayer.Url}"
VideoSelected="True"
AreTransportControlsEnabled="True">
</formsVideoLibrary:VideoPlayer>
</Grid>
</ContentPage.Content>
Any suggestions or insight would be appreciated.