d8306c8182
- Added ability to completely collapse command bars (check settings) - Added feature that checks your clipboard and suggests you to open YouTube page in the app if there is any (check settings) - Added additional analytics tools to detect authorization fails - Added video speed controller (check video settings) - Test ads are now shown - Fixed gaps in grids - Fixed some cases when on maximizing video it pauses/continues - Fixed missing inbox items due to incompatible date formats - Fixed inability to unsubscribe from channel - Fixed minimization of videos with unusual aspect ratios - Fixed some cases when video continues to play in the background after closing/reloading video page
533 lines
19 KiB
C#
533 lines
19 KiB
C#
using FoxTube.Controls;
|
|
using FoxTube.Controls.Adverts;
|
|
using FoxTube.Controls.Player;
|
|
using Google.Apis.YouTube.v3.Data;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Windows.ApplicationModel.Resources;
|
|
using Windows.Foundation;
|
|
using Windows.Graphics.Display;
|
|
using Windows.Media.Core;
|
|
using Windows.UI.Xaml;
|
|
using Windows.UI.Xaml.Controls;
|
|
using Windows.UI.Xaml.Controls.Primitives;
|
|
using YoutubeExplode;
|
|
using YoutubeExplode.Models.ClosedCaptions;
|
|
using YoutubeExplode.Models.MediaStreams;
|
|
|
|
namespace FoxTube
|
|
{
|
|
public delegate void MinimodeChangedEventHandler(object sender, bool isOn);
|
|
|
|
public enum PlayerDisplayState { Normal, Minimized, Compact }
|
|
|
|
public class QualityComparer : IComparer<string>
|
|
{
|
|
public int Compare(string x, string y)
|
|
{
|
|
string[] xArr = x.Split('p');
|
|
string[] yArr = y.Split('p');
|
|
|
|
int qualityA = int.Parse(xArr[0]);
|
|
int qualityB = int.Parse(yArr[0]);
|
|
int framerateA = 30;
|
|
int framerateB = 30;
|
|
|
|
if (!string.IsNullOrWhiteSpace(xArr[1]))
|
|
framerateA = int.Parse(xArr[1]);
|
|
if (!string.IsNullOrWhiteSpace(yArr[1]))
|
|
framerateB = int.Parse(yArr[1]);
|
|
|
|
if (qualityA > qualityB)
|
|
return 1;
|
|
else if (qualityA < qualityB)
|
|
return -1;
|
|
else
|
|
return framerateA - framerateB > 0 ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Custom controls for media player. MARKUP IS IN **Themes/Generic.xaml**!!!
|
|
/// </summary>
|
|
public sealed class PlayerControls : MediaTransportControls
|
|
{
|
|
public event RoutedEventHandler CloseRequested;
|
|
public event MinimodeChangedEventHandler MiniModeChanged;
|
|
public event RoutedEventHandler NextRequested;
|
|
|
|
#region Controls variables
|
|
Button minimize;
|
|
Button close;
|
|
Button miniview;
|
|
Button play;
|
|
Button next;
|
|
Button volumeMenu;
|
|
Button mute;
|
|
Button live;
|
|
Button fwd;
|
|
Button bwd;
|
|
Button captionsMenu;
|
|
Button drag;
|
|
|
|
TextBlock title;
|
|
TextBlock channel;
|
|
TextBlock elapsed;
|
|
TextBlock remain;
|
|
|
|
Slider volume;
|
|
Slider seek;
|
|
Slider playbackSpeed;
|
|
ProgressBar seekIndicator;
|
|
|
|
ComboBox captions;
|
|
ComboBox quality;
|
|
|
|
ToggleSwitch captionsSwitch;
|
|
|
|
StackPanel rightFooter;
|
|
StackPanel leftFooter;
|
|
StackPanel rightHeader;
|
|
StackPanel centerStack;
|
|
Grid header;
|
|
Grid footer;
|
|
Grid center;
|
|
Grid centerTrigger;
|
|
#endregion
|
|
|
|
PlayerDisplayState State { get; set; } = PlayerDisplayState.Normal;
|
|
|
|
public MediaElement Player { get; set; }
|
|
public PlayerAdvert Advert;
|
|
public LiveCaptions Caption;
|
|
|
|
TimeSpan timecodeBackup;
|
|
bool needUpdateTimecode = false;
|
|
|
|
public Video Meta { get; set; }
|
|
|
|
public IReadOnlyList<ClosedCaptionTrackInfo> ClosedCaptions { get; set; }
|
|
public MediaStreamInfoSet MediaStreams { get; set; }
|
|
|
|
Queue<Action> queue = new Queue<Action>();
|
|
bool isReady = false;
|
|
|
|
public PlayerControls()
|
|
{
|
|
DefaultStyleKey = typeof(PlayerControls);
|
|
}
|
|
|
|
protected override void OnApplyTemplate()
|
|
{
|
|
AssignControls();
|
|
|
|
isReady = true;
|
|
|
|
minimize.Click += Minimize_Click;
|
|
close.Click += Close_Click;
|
|
miniview.Click += Miniview_Click;
|
|
|
|
next.Click += Next_Click;
|
|
volume.ValueChanged += Volume_ValueChanged;
|
|
playbackSpeed.ValueChanged += PlaybackSpeed_ValueChanged;
|
|
live.Click += Live_Click;
|
|
|
|
captionsSwitch.Toggled += CaptionsSwitch_Toggled;
|
|
captions.SelectionChanged += Captions_SelectionChanged;
|
|
quality.SelectionChanged += Quality_SelectionChanged;
|
|
seek.ValueChanged += Seek_ValueChanged;
|
|
|
|
Player.Tapped += (s, e) =>
|
|
{
|
|
Rect view = new Rect(0, 0, centerTrigger.ActualWidth, centerTrigger.ActualHeight);
|
|
Point p = e.GetPosition(centerTrigger);
|
|
|
|
if (!view.Contains(p) || e.PointerDeviceType != Windows.Devices.Input.PointerDeviceType.Mouse || State != PlayerDisplayState.Normal)
|
|
return;
|
|
|
|
if (Player.CurrentState == Windows.UI.Xaml.Media.MediaElementState.Playing)
|
|
Player.Pause();
|
|
else if (Player.CurrentState == Windows.UI.Xaml.Media.MediaElementState.Paused)
|
|
Player.Play();
|
|
};
|
|
|
|
if (queue.Count > 0)
|
|
foreach (Action i in queue)
|
|
i();
|
|
|
|
base.OnApplyTemplate();
|
|
}
|
|
|
|
private void PlaybackSpeed_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
|
|
{
|
|
Player.PlaybackRate = playbackSpeed.Value;
|
|
}
|
|
|
|
void AssignControls()
|
|
{
|
|
minimize = GetTemplateChild("MinimizeButton") as Button;
|
|
close = GetTemplateChild("CloseButton") as Button;
|
|
miniview = GetTemplateChild("CompactOverlayButton") as Button;
|
|
play = GetTemplateChild("PlayPauseButton") as Button;
|
|
next = GetTemplateChild("NextButton") as Button;
|
|
volumeMenu = GetTemplateChild("VolumeMenuButton") as Button;
|
|
mute = GetTemplateChild("AudioMuteButton") as Button;
|
|
live = GetTemplateChild("PlayLiveButton") as Button;
|
|
fwd = GetTemplateChild("SkipForwardButton") as Button;
|
|
bwd = GetTemplateChild("SkipBackwardButton") as Button;
|
|
captionsMenu = GetTemplateChild("CaptionsMenuButton") as Button;
|
|
drag = GetTemplateChild("drag") as Button;
|
|
|
|
Advert = GetTemplateChild("AdvertControl") as PlayerAdvert;
|
|
Caption = GetTemplateChild("CaptionControl") as LiveCaptions;
|
|
|
|
title = GetTemplateChild("title") as TextBlock;
|
|
channel = GetTemplateChild("channel") as TextBlock;
|
|
elapsed = GetTemplateChild("TimeElapsedElement") as TextBlock;
|
|
remain = GetTemplateChild("TimeRemainingElement") as TextBlock;
|
|
|
|
volume = GetTemplateChild("VolumeSlider") as Slider;
|
|
seek = GetTemplateChild("ProgressSlider") as Slider;
|
|
playbackSpeed = GetTemplateChild("PlaybackSpeedSlider") as Slider;
|
|
seekIndicator = GetTemplateChild("SeekIndicator") as ProgressBar;
|
|
|
|
captions = GetTemplateChild("CaptionsSelector") as ComboBox;
|
|
quality = GetTemplateChild("QualitySelector") as ComboBox;
|
|
captionsSwitch = GetTemplateChild("CaptionsToggleSwitch") as ToggleSwitch;
|
|
|
|
rightFooter = GetTemplateChild("RightFooterControls") as StackPanel;
|
|
leftFooter = GetTemplateChild("LeftFooterControls") as StackPanel;
|
|
rightHeader = GetTemplateChild("RightHeaderControls") as StackPanel;
|
|
centerStack = GetTemplateChild("centerControls") as StackPanel;
|
|
header = GetTemplateChild("header") as Grid;
|
|
footer = GetTemplateChild("footer") as Grid;
|
|
center = GetTemplateChild("center") as Grid;
|
|
centerTrigger = GetTemplateChild("centerTrigger") as Grid;
|
|
}
|
|
|
|
private void Seek_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
|
|
{
|
|
seekIndicator.Value = seek.Value;
|
|
}
|
|
|
|
private async void Quality_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (Meta.Snippet.LiveBroadcastContent == "live")
|
|
goto SetQuality;
|
|
if(!needUpdateTimecode)
|
|
timecodeBackup = Player.Position;
|
|
needUpdateTimecode = true;
|
|
Player.Pause();
|
|
Player.Source = null;
|
|
|
|
SetQuality:
|
|
object info = (quality.SelectedItem as ComboBoxItem).Tag;
|
|
if (info is MuxedStreamInfo)
|
|
Player.SetPlaybackSource(MediaSource.CreateFromUri((info as MuxedStreamInfo).Url.ToUri()));
|
|
else if (info is VideoStreamInfo || info == null)
|
|
Player.SetPlaybackSource(MediaSource.CreateFromUri(await ManifestGenerator.GetManifest(Meta, info as VideoStreamInfo, MediaStreams)));
|
|
else if (info is StreamQuality)
|
|
Player.SetPlaybackSource(MediaSource.CreateFromUri((info as StreamQuality).Url));
|
|
}
|
|
|
|
private void Captions_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if(Caption.IsActive)
|
|
{
|
|
Caption.Close();
|
|
Caption.Initialize((captions.SelectedItem as ComboBoxItem).Tag as ClosedCaptionTrackInfo);
|
|
}
|
|
}
|
|
|
|
private void CaptionsSwitch_Toggled(object sender, RoutedEventArgs e)
|
|
{
|
|
if(captionsSwitch.IsOn)
|
|
Caption.Initialize((captions.SelectedItem as ComboBoxItem).Tag as ClosedCaptionTrackInfo);
|
|
else
|
|
Caption.Close();
|
|
}
|
|
|
|
private void Live_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
Player.Position = Player.NaturalDuration.TimeSpan;
|
|
}
|
|
|
|
private void Next_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
NextRequested.Invoke(sender, e);
|
|
}
|
|
|
|
private void Miniview_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (State == PlayerDisplayState.Compact)
|
|
Maximize();
|
|
else
|
|
EnterMiniview();
|
|
}
|
|
|
|
public void UpdateVolumeIcon()
|
|
{
|
|
Volume_ValueChanged(this, null);
|
|
}
|
|
|
|
private void Volume_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
|
|
{
|
|
double v = volume.Value;
|
|
if (v == 0 || Player.IsMuted)
|
|
volumeMenu.Content = mute.Content = "\xE74F";
|
|
else if (v <= 25 && v > 0)
|
|
volumeMenu.Content = mute.Content = "\xE992";
|
|
else if (v <= 50 && v > 25)
|
|
volumeMenu.Content = mute.Content = "\xE993";
|
|
else if (v <= 75 && v > 50)
|
|
volumeMenu.Content = mute.Content = "\xE994";
|
|
else if (v > 75)
|
|
volumeMenu.Content = mute.Content = "\xE995";
|
|
}
|
|
|
|
private void Player_MediaOpened(object sender, RoutedEventArgs args)
|
|
{
|
|
if (!needUpdateTimecode)
|
|
return;
|
|
|
|
needUpdateTimecode = false;
|
|
Player.Position = timecodeBackup;
|
|
}
|
|
|
|
private void Close_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
CloseRequested?.Invoke(sender, e);
|
|
}
|
|
|
|
private void Minimize_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (State == PlayerDisplayState.Normal)
|
|
Minimize();
|
|
else
|
|
Maximize();
|
|
}
|
|
|
|
public void Minimize()
|
|
{
|
|
if (State == PlayerDisplayState.Minimized)
|
|
return;
|
|
|
|
header.Children.Remove(minimize);
|
|
center.Children.Add(minimize);
|
|
rightHeader.Children.Remove(close);
|
|
center.Children.Add(close);
|
|
leftFooter.Children.Remove(play);
|
|
centerStack.Children.Add(play);
|
|
rightFooter.Children.Remove(fwd);
|
|
centerStack.Children.Add(fwd);
|
|
rightFooter.Children.Remove(bwd);
|
|
centerStack.Children.Insert(0, bwd);
|
|
|
|
header.Visibility = Visibility.Collapsed;
|
|
center.Visibility = Visibility.Visible;
|
|
footer.Visibility = Visibility.Collapsed;
|
|
|
|
minimize.Content = "\xE010";
|
|
|
|
MiniModeChanged.Invoke(this, true);
|
|
Caption.Minimize();
|
|
|
|
State = PlayerDisplayState.Minimized;
|
|
}
|
|
|
|
public void Maximize()
|
|
{
|
|
if (State == PlayerDisplayState.Normal)
|
|
return;
|
|
|
|
if(State == PlayerDisplayState.Compact)
|
|
{
|
|
center.Children.Remove(miniview);
|
|
rightHeader.Children.Add(miniview);
|
|
centerStack.Children.Remove(play);
|
|
leftFooter.Children.Insert(0, play);
|
|
centerStack.Children.Remove(fwd);
|
|
rightFooter.Children.Insert(0, fwd);
|
|
centerStack.Children.Remove(bwd);
|
|
rightFooter.Children.Insert(0, bwd);
|
|
|
|
miniview.Margin = new Thickness();
|
|
miniview.Width = 50;
|
|
miniview.Height = 50;
|
|
}
|
|
else
|
|
{
|
|
center.Children.Remove(minimize);
|
|
header.Children.Insert(0, minimize);
|
|
center.Children.Remove(close);
|
|
rightHeader.Children.Insert(0, close);
|
|
centerStack.Children.Remove(play);
|
|
leftFooter.Children.Insert(0, play);
|
|
centerStack.Children.Remove(fwd);
|
|
rightFooter.Children.Insert(0, fwd);
|
|
centerStack.Children.Remove(bwd);
|
|
rightFooter.Children.Insert(0, bwd);
|
|
|
|
MiniModeChanged.Invoke(this, false);
|
|
}
|
|
|
|
drag.Visibility = Visibility.Collapsed;
|
|
header.Visibility = Visibility.Visible;
|
|
center.Visibility = Visibility.Collapsed;
|
|
footer.Visibility = Visibility.Visible;
|
|
miniview.Content = "\xE2B3";
|
|
minimize.Content = "\xE011";
|
|
Caption.Maximize();
|
|
|
|
State = PlayerDisplayState.Normal;
|
|
}
|
|
|
|
public void EnterMiniview()
|
|
{
|
|
if (State == PlayerDisplayState.Compact)
|
|
return;
|
|
|
|
rightHeader.Children.Remove(miniview);
|
|
center.Children.Add(miniview);
|
|
leftFooter.Children.Remove(play);
|
|
centerStack.Children.Add(play);
|
|
rightFooter.Children.Remove(fwd);
|
|
centerStack.Children.Add(fwd);
|
|
rightFooter.Children.Remove(bwd);
|
|
centerStack.Children.Insert(0, bwd);
|
|
|
|
drag.Visibility = Visibility.Visible;
|
|
header.Visibility = Visibility.Collapsed;
|
|
center.Visibility = Visibility.Visible;
|
|
footer.Visibility = Visibility.Collapsed;
|
|
|
|
miniview.Margin = new Thickness(0, 32, 0, 0);
|
|
miniview.Width = 47;
|
|
miniview.Height = 47;
|
|
|
|
miniview.Content = "\xE2B4";
|
|
Caption.Minimize();
|
|
|
|
State = PlayerDisplayState.Compact;
|
|
}
|
|
|
|
public async void Load(Video meta)
|
|
{
|
|
if(!isReady)
|
|
{
|
|
queue.Enqueue(() => Load(meta));
|
|
return;
|
|
}
|
|
|
|
Player.MediaOpened += Player_MediaOpened;
|
|
|
|
Meta = meta;
|
|
title.Text = meta.Snippet.Title;
|
|
channel.Text = meta.Snippet.ChannelTitle;
|
|
|
|
MediaStreams = await new YoutubeClient().GetVideoMediaStreamInfosAsync(meta.Id);
|
|
|
|
if (meta.Snippet.LiveBroadcastContent == "none")
|
|
{
|
|
ClosedCaptions = await new YoutubeClient().GetVideoClosedCaptionTrackInfosAsync(meta.Id);
|
|
|
|
uint screenHeight = DisplayInformation.GetForCurrentView().ScreenHeightInRawPixels;
|
|
|
|
List<string> qualityList = MediaStreams.GetAllVideoQualityLabels().ToList();
|
|
qualityList.Sort(new QualityComparer());
|
|
qualityList.Reverse();
|
|
|
|
quality.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/auto")
|
|
});
|
|
|
|
foreach (string i in qualityList)
|
|
{
|
|
object tag;
|
|
if (MediaStreams.Muxed.Any(m => m.VideoQualityLabel == i && m.Resolution.Height <= screenHeight))
|
|
tag = MediaStreams.Muxed.Find(m => m.VideoQualityLabel == i);
|
|
else if (MediaStreams.Video.Any(m => m.VideoQualityLabel == i && m.Resolution.Height <= screenHeight && m.Container.GetFileExtension() == "mp4"))
|
|
tag = MediaStreams.Video.Find(m => m.VideoQualityLabel == i && m.Container.GetFileExtension() == "mp4");
|
|
else
|
|
continue;
|
|
|
|
quality.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = i,
|
|
Tag = tag
|
|
});
|
|
}
|
|
|
|
string s = SettingsStorage.VideoQuality == "remember" ? SettingsStorage.RememberedQuality : SettingsStorage.VideoQuality;
|
|
|
|
if (quality.Items.Any(i => ((i as ComboBoxItem).Content as string).Contains(s)))
|
|
quality.SelectedItem = quality.Items.Find(i => ((i as ComboBoxItem).Content as string).Contains(s));
|
|
else
|
|
quality.SelectedIndex = 0;
|
|
|
|
|
|
if (ClosedCaptions.Count == 0)
|
|
{
|
|
captionsMenu.Visibility = Visibility.Collapsed;
|
|
return;
|
|
}
|
|
|
|
foreach (ClosedCaptionTrackInfo i in ClosedCaptions)
|
|
captions.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = string.Format("{0} {1}", CultureInfo.GetCultureInfo(i.Language.Code).DisplayName, i.IsAutoGenerated ? ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/generatedCaption") : ""),
|
|
Tag = i
|
|
});
|
|
|
|
ClosedCaptionTrackInfo item = ClosedCaptions.Find(i => SettingsStorage.RelevanceLanguage.Contains(i.Language.Code)) ?? ClosedCaptions.Find(i => "en-US".Contains(i.Language.Code));
|
|
if (item == null)
|
|
item = ClosedCaptions.First();
|
|
|
|
captions.SelectedItem = captions.Items.Find(i => (i as ComboBoxItem).Tag as ClosedCaptionTrackInfo == item);
|
|
|
|
Caption.Player = Player;
|
|
}
|
|
else
|
|
{
|
|
captionsMenu.Visibility = Visibility.Collapsed;
|
|
seek.Visibility = Visibility.Collapsed;
|
|
live.Visibility = Visibility.Visible;
|
|
remain.Visibility = Visibility.Collapsed;
|
|
elapsed.FontSize = 24;
|
|
Grid.SetRow(elapsed, 0);
|
|
Grid.SetRowSpan(elapsed, 2);
|
|
elapsed.HorizontalAlignment = HorizontalAlignment.Right;
|
|
fwd.Visibility = Visibility.Collapsed;
|
|
bwd.Visibility = Visibility.Collapsed;
|
|
|
|
List<StreamQuality> list = await ManifestGenerator.ResolveLiveSteream(MediaStreams.HlsLiveStreamUrl);
|
|
|
|
foreach (StreamQuality i in list)
|
|
quality.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = i.Resolution,
|
|
Tag = i
|
|
});
|
|
|
|
string s = SettingsStorage.VideoQuality == "remember" ? SettingsStorage.RememberedQuality : SettingsStorage.VideoQuality;
|
|
|
|
if (quality.Items.Any(i => (i as ComboBoxItem).Content as string == s))
|
|
quality.SelectedItem = quality.Items.Find(i => (i as ComboBoxItem).Content as string == s);
|
|
else
|
|
quality.SelectedIndex = 0;
|
|
}
|
|
|
|
Focus(FocusState.Programmatic);
|
|
}
|
|
|
|
public void PushAdvert()
|
|
{
|
|
if(State == PlayerDisplayState.Normal)
|
|
Advert.PushAdvert();
|
|
}
|
|
}
|
|
}
|