1.0-dev6 (Option bar added and code cleaned)

This commit is contained in:
2022-02-19 01:38:37 +01:00
Unverified
parent 3c274ff4fc
commit 96a7953500
51 changed files with 856 additions and 423 deletions

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using Windows.Media.Editing;
using Windows.Storage;
namespace VDownload.Core.Services
{
public class Config
{
#region CONSTANTS
// SETTINGS CONTAINER
private static readonly ApplicationDataContainer SettingsContainer = ApplicationData.Current.LocalSettings;
// DEFAULT SETTINGS
private static readonly Dictionary<string, object> DefaultSettings = new Dictionary<string, object>()
{
{ "delete_temp_on_start", true },
{ "twitch_vod_passive_trim", true },
{ "twitch_vod_downloading_chunk_retry_after_error", true },
{ "twitch_vod_downloading_chunk_max_retries", 10 },
{ "twitch_vod_downloading_chunk_retries_delay", 5000 },
{ "media_transcoding_use_hardware_acceleration", true },
{ "media_transcoding_use_mrfcrf444_algorithm", true },
{ "media_editing_algorithm", (int)MediaTrimmingPreference.Fast },
{ "default_max_playlist_videos", 0 },
};
#endregion
#region METHODS
// GET VALUE
public static object GetValue(string key)
{
return SettingsContainer.Values[key];
}
// SET VALUE
public static void SetValue(string key, object value)
{
SettingsContainer.Values[key] = value;
}
// SET DEFAULT
public static void SetDefault()
{
foreach (KeyValuePair<string, object> s in DefaultSettings)
{
SettingsContainer.Values[s.Key] = s.Value;
}
}
// REBUILD
public static void Rebuild()
{
foreach (KeyValuePair<string, object> s in DefaultSettings)
{
if (!SettingsContainer.Values.ContainsKey(s.Key))
{
SettingsContainer.Values[s.Key] = s.Value;
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using Windows.Foundation;
using Windows.Media.Editing;
using Windows.Media.MediaProperties;
using Windows.Media.Transcoding;
using Windows.Storage;
using Windows.Storage.Streams;
namespace VDownload.Core.Services
{
public class MediaProcessor
{
#region CONSTRUCTOR
public MediaProcessor(StorageFile outputFile, TimeSpan trimStart, TimeSpan trimEnd)
{
OutputFile = outputFile;
TrimStart = trimStart;
TrimEnd = trimEnd;
}
#endregion
#region PROPERTIES
public StorageFile OutputFile { get; private set; }
public TimeSpan TrimStart { get; private set; }
public TimeSpan TrimEnd { get; private set; }
#endregion
#region STANDARD METHODS
public async Task Run(StorageFile audioVideoInputFile, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default)
{
// Invoke ProcessingStarted event
ProcessingStarted?.Invoke(this, EventArgs.Empty);
// Init transcoder
MediaTranscoder mediaTranscoder = new MediaTranscoder
{
HardwareAccelerationEnabled = (bool)Config.GetValue("media_processor_use_hardware_acceleration"),
VideoProcessingAlgorithm = (bool)Config.GetValue("media_processor_use_mrfcrf444_algorithm") ? MediaVideoProcessingAlgorithm.MrfCrf444 : MediaVideoProcessingAlgorithm.Default,
TrimStartTime = TrimStart,
TrimStopTime = TrimEnd,
};
// Start transcoding operation
using (IRandomAccessStream outputFileOpened = await OutputFile.OpenAsync(FileAccessMode.ReadWrite))
{
PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await audioVideoInputFile.OpenAsync(FileAccessMode.Read), outputFileOpened, await GetMediaEncodingProfile(audioVideoInputFile, extension, mediaType));
IAsyncActionWithProgress<double> transcodingTask = transcodingPreparated.TranscodeAsync();
try
{
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(percent), null)); }));
await outputFileOpened.FlushAsync();
}
catch (TaskCanceledException) { }
transcodingTask.Close();
}
// Invoke ProcessingCompleted event
ProcessingCompleted?.Invoke(this, EventArgs.Empty);
}
public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default)
{
// Invoke ProcessingStarted event
ProcessingStarted?.Invoke(this, EventArgs.Empty);
// Init editor
MediaComposition mediaEditor = new MediaComposition();
// Add media files
Task<MediaClip> getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask();
Task<BackgroundAudioTrack> getAudioFileTask = BackgroundAudioTrack.CreateFromFileAsync(audioFile).AsTask();
await Task.WhenAll(getVideoFileTask, getAudioFileTask);
MediaClip videoElement = getVideoFileTask.Result;
videoElement.TrimTimeFromStart = TrimStart;
videoElement.TrimTimeFromEnd = TrimEnd;
BackgroundAudioTrack audioElement = getAudioFileTask.Result;
audioElement.TrimTimeFromStart = TrimStart;
audioElement.TrimTimeFromEnd = TrimEnd;
mediaEditor.Clips.Add(getVideoFileTask.Result);
mediaEditor.BackgroundAudioTracks.Add(getAudioFileTask.Result);
// Start rendering operation
var renderOperation = mediaEditor.RenderToFileAsync(OutputFile, (MediaTrimmingPreference)Config.GetValue("media_editing_algorithm"), await GetMediaEncodingProfile(videoFile, audioFile, (MediaFileExtension)extension, MediaType.AudioVideo));
renderOperation.Progress += (info, progress) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(progress), null)); };
await renderOperation.AsTask(cancellationToken);
// Invoke ProcessingCompleted event
ProcessingCompleted?.Invoke(this, EventArgs.Empty);
}
public async Task Run(StorageFile audioFile, AudioFileExtension extension, CancellationToken cancellationToken = default) { await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, cancellationToken); }
public async Task Run(StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default) { await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, cancellationToken); }
#endregion
#region LOCAL METHODS
// GET ENCODING PROFILE
public static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType)
{
// Create profile object
MediaEncodingProfile profile;
// Set extension
switch (extension)
{
default:
case MediaFileExtension.MP4: profile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.WMV: profile = MediaEncodingProfile.CreateWmv(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.HEVC: profile = MediaEncodingProfile.CreateHevc(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.MP3: profile = MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High); break;
case MediaFileExtension.FLAC: profile = MediaEncodingProfile.CreateFlac(AudioEncodingQuality.High); break;
case MediaFileExtension.WAV: profile = MediaEncodingProfile.CreateWav(AudioEncodingQuality.High); break;
case MediaFileExtension.M4A: profile = MediaEncodingProfile.CreateM4a(AudioEncodingQuality.High); break;
case MediaFileExtension.ALAC: profile = MediaEncodingProfile.CreateAlac(AudioEncodingQuality.High); break;
case MediaFileExtension.WMA: profile = MediaEncodingProfile.CreateWma(AudioEncodingQuality.High); break;
}
// Set video parameters
if (mediaType != MediaType.OnlyAudio)
{
var videoData = await videoFile.Properties.GetVideoPropertiesAsync();
profile.Video.Height = videoData.Height;
profile.Video.Width = videoData.Width;
profile.Video.Bitrate = videoData.Bitrate;
}
// Set audio parameters
if (mediaType != MediaType.OnlyVideo)
{
var audioData = await audioFile.Properties.GetMusicPropertiesAsync();
profile.Audio.Bitrate = audioData.Bitrate;
if (mediaType == MediaType.AudioVideo) profile.Video.Bitrate -= audioData.Bitrate;
}
// Delete audio tracks
if (mediaType == MediaType.OnlyVideo)
{
var audioTracks = profile.GetAudioTracks();
audioTracks.Clear();
profile.SetAudioTracks(audioTracks.AsEnumerable());
}
// Return profile
return profile;
}
public static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType) { return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType); }
#endregion
#region EVENT HANDLERS
public event EventHandler ProcessingStarted;
public event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
public event EventHandler ProcessingCompleted;
#endregion
}
}

View File

@@ -0,0 +1,121 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
namespace VDownload.Core.Services.Sources.Twitch
{
public class Auth
{
#region CONSTANTS
// CLIENT ID
public readonly static string ClientID = "yukkqkwp61wsv3u1pya17crpyaa98y";
// GQL API CLIENT ID
public readonly static string GQLApiClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko";
// REDIRECT URL
public readonly static Uri RedirectUrl = new Uri("https://www.vd.com");
// AUTHORIZATION URL
private readonly static string ResponseType = "token";
private readonly static string[] Scopes = new[]
{
"user:read:subscriptions",
};
public readonly static Uri AuthorizationUrl = new Uri($"https://id.twitch.tv/oauth2/authorize?client_id={ClientID}&redirect_uri={RedirectUrl.OriginalString}&response_type={ResponseType}&scope={string.Join(" ", Scopes)}");
#endregion
#region METHODS
// READ ACCESS TOKEN
public static async Task<string> ReadAccessTokenAsync()
{
try
{
// Get file
StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.GetFolderAsync("AuthData");
StorageFile authDataFile = await authDataFolder.GetFileAsync("Twitch.auth");
// Return data
return await FileIO.ReadTextAsync(authDataFile);
}
catch (FileNotFoundException)
{
return null;
}
}
// SAVE ACCESS TOKEN
public static async Task SaveAccessTokenAsync(string accessToken)
{
// Get file
StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("AuthData", CreationCollisionOption.OpenIfExists);
StorageFile authDataFile = await authDataFolder.CreateFileAsync("Twitch.auth", CreationCollisionOption.ReplaceExisting);
// Save data
FileIO.WriteTextAsync(authDataFile, accessToken);
}
// DELETE ACCESS TOKEN
public static async Task DeleteAccessTokenAsync()
{
try
{
// Get file
StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.GetFolderAsync("AuthData");
StorageFile authDataFile = await authDataFolder.GetFileAsync("Twitch.auth");
// Delete file
await authDataFile.DeleteAsync();
}
catch (FileNotFoundException) { }
}
// VALIDATE ACCESS TOKEN
public static async Task<(bool IsValid, string Login, DateTime? ExpirationDate)> ValidateAccessTokenAsync(string accessToken)
{
// Create client
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
client.Headers.Add("Authorization", $"Bearer {accessToken}");
try
{
// Check access token
JObject response = JObject.Parse(await client.DownloadStringTaskAsync("https://id.twitch.tv/oauth2/validate"));
string login = response["login"].ToString();
DateTime? expirationDate = DateTime.Now.AddSeconds(long.Parse(response["expires_in"].ToString()));
return (true, login, expirationDate);
}
catch (WebException)
{
return (false, null, null);
}
}
// REVOKE ACCESS TOKEN
public static async Task RevokeAccessTokenAsync(string accessToken)
{
// Create client
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
// Revoke access token
await client.UploadStringTaskAsync(new Uri("https://id.twitch.tv/oauth2/revoke"), $"client_id={ClientID}&token={accessToken}");
}
#endregion
}
}

View File

@@ -0,0 +1,126 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services.Sources.Twitch
{
public class Channel : IPlaylistService
{
#region CONSTRUCTORS
public Channel(string id)
{
ID = id;
}
#endregion
#region PROPERTIES
public string ID { get; private set; }
public string Name { get; private set; }
public Vod[] Videos { get; private set; }
#endregion
#region STANDARD METHODS
// GET CHANNEL METADATA
public async Task GetMetadataAsync()
{
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Check access token
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
// Create client
WebClient client = new WebClient();
client.Headers.Add("Authorization", $"Bearer {accessToken}");
client.Headers.Add("Client-Id", Auth.ClientID);
// Get response
client.QueryString.Add("login", ID);
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"][0];
// Set parameters
if (!ID.All(char.IsDigit)) ID = (string)response["id"];
Name = (string)response["display_name"];
}
// GET CHANNEL VIDEOS
public async Task GetVideosAsync(int numberOfVideos)
{
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Set pagination
string pagination = "";
// Set array of videos
List<Vod> videos = new List<Vod>();
// Get videos
int count;
JToken[] videosData;
List<Task> getStreamsTasks = new List<Task>();
do
{
// Check access token
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
// Create client
WebClient client = new WebClient();
client.Headers.Add("Authorization", $"Bearer {accessToken}");
client.Headers.Add("Client-Id", Auth.ClientID);
// Set number of videos to get in this iteration
count = numberOfVideos < 100 ? numberOfVideos : 100;
// Get response
client.QueryString.Add("user_id", ID);
client.QueryString.Add("first", count.ToString());
client.QueryString.Add("after", pagination);
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos"));
pagination = (string)response["pagination"]["cursor"];
videosData = response["data"].ToArray();
foreach (JToken videoData in videosData)
{
Vod video = new Vod((string)videoData["id"]);
video.GetMetadataAsync(videoData);
getStreamsTasks.Add(video.GetStreamsAsync());
videos.Add(video);
numberOfVideos--;
}
}
while (numberOfVideos > 0 && count == videosData.Length);
// Wait for all getStreams tasks
await Task.WhenAll(getStreamsTasks);
// Set Videos parameter
Videos = videos.ToArray();
}
#endregion
}
}

View File

@@ -0,0 +1,189 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Objects;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch
{
public class Clip : IVideoService
{
#region CONSTANTS
#endregion
#region CONSTRUCTORS
public Clip(string id)
{
ID = id;
}
#endregion
#region PROPERTIES
public string ID { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public DateTime Date { get; private set; }
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public Stream[] Streams { get; private set; }
#endregion
#region STANDARD METHODS
// GET CLIP METADATA
public async Task GetMetadataAsync()
{
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Check access token
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
// Create client
WebClient client = new WebClient();
client.Headers.Add("Authorization", $"Bearer {accessToken}");
client.Headers.Add("Client-Id", Auth.ClientID);
// Get response
client.QueryString.Add("id", ID);
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/clips")).GetValue("data")[0];
// Set parameters
Title = (string)response["title"];
Author = (string)response["broadcaster_name"];
Date = Convert.ToDateTime(response["created_at"]);
Duration = TimeSpan.FromSeconds((double)response["duration"]);
Views = (long)response["view_count"];
Thumbnail = new Uri((string)response["thumbnail_url"]);
}
public async Task GetStreamsAsync()
{
// Create client
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
client.Headers.Add("Client-ID", Auth.GQLApiClientID);
// Get video streams
JToken[] response = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["videoQualities"].ToArray();
// Init streams list
List<Stream> streams = new List<Stream>();
// Parse response
foreach (JToken streamData in response)
{
// Get info
Uri url = new Uri((string)streamData["sourceURL"]);
int height = int.Parse((string)streamData["quality"]);
int frameRate = (int)streamData["frameRate"];
// Create stream
Stream stream = new Stream(url, false, StreamType.AudioVideo)
{
Height = height,
FrameRate = frameRate
};
// Add stream
streams.Add(stream);
}
// Set Streams parameter
Streams = streams.ToArray();
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Invoke DownloadingStarted event
DownloadingStarted?.Invoke(this, EventArgs.Empty);
// Create client
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
// Get video GQL access token
JToken videoAccessToken = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["playbackAccessToken"];
// Download
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.mp4");
using (client = new WebClient())
{
client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new ProgressChangedEventArgs(a.ProgressPercentage, null)); };
client.QueryString.Add("sig", (string)videoAccessToken["signature"]);
client.QueryString.Add("token", HttpUtility.UrlEncode((string)videoAccessToken["value"]));
using (cancellationToken.Register(client.CancelAsync))
{
await client.DownloadFileTaskAsync(audioVideoStream.Url, rawFile.Path);
}
}
DownloadingCompleted?.Invoke(this, EventArgs.Empty);
// Processing
StorageFile outputFile = rawFile;
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart > new TimeSpan(0) || trimEnd < Duration)
{
outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd);
mediaProcessor.ProcessingStarted += ProcessingStarted;
mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged;
mediaProcessor.ProcessingCompleted += ProcessingCompleted;
await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken);
}
// Return output file
return outputFile;
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioVideoStream, extension, mediaType, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
#endregion
#region EVENT HANDLERS
public event EventHandler DownloadingStarted;
public event EventHandler<ProgressChangedEventArgs> DownloadingProgressChanged;
public event EventHandler DownloadingCompleted;
public event EventHandler ProcessingStarted;
public event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
public event EventHandler ProcessingCompleted;
#endregion
}
}

View File

@@ -0,0 +1,324 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Objects;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch
{
public class Vod : IVideoService
{
#region CONSTANTS
// METADATA TIME FORMATS
private static readonly string[] TimeFormats = new[]
{
@"h\hm\ms\s",
@"m\ms\s",
@"s\s",
};
// STREAMS RESPONSE REGULAR EXPRESSIONS
private static readonly Regex L2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""(?<video_codec>\S+),(?<audio_codec>\S+)"",RESOLUTION=(?<width>\d+)x(?<height>\d+),VIDEO=""\w+"",FRAME-RATE=(?<frame_rate>\d+.\d+)");
// CHUNK RESPONSE REGULAR EXPRESSION
private static readonly Regex ChunkRegex = new Regex(@"#EXTINF:(?<duration>\d+.\d+),\n(?<filename>\S+.ts)");
#endregion
#region CONSTRUCTORS
public Vod(string id)
{
ID = id;
}
#endregion
#region PROPERTIES
public string ID { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public DateTime Date { get; private set; }
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public Stream[] Streams { get; private set; }
#endregion
#region STANDARD METHODS
// GET VOD METADATA
public async Task GetMetadataAsync()
{
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Check access token
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
// Create client
WebClient client = new WebClient();
client.Headers.Add("Authorization", $"Bearer {accessToken}");
client.Headers.Add("Client-Id", Auth.ClientID);
// Get response
client.QueryString.Add("id", ID);
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0];
// Set parameters
GetMetadataAsync(response);
}
internal void GetMetadataAsync(JToken response)
{
// Set parameters
Title = ((string)response["title"]).Replace("\n", "");
Author = (string)response["user_name"];
Date = Convert.ToDateTime(response["created_at"]);
Duration = TimeSpan.ParseExact((string)response["duration"], TimeFormats, null);
Views = (long)response["view_count"];
Thumbnail = (string)response["thumbnail_url"] == string.Empty ? null : new Uri((string)response["thumbnail_url"]);
}
// GET VOD STREAMS
public async Task GetStreamsAsync()
{
// Create client
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
// Get video GQL access token
JToken videoAccessToken = JObject.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "{\"operationName\":\"PlaybackAccessToken_Template\",\"query\":\"query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}\",\"variables\":{\"isLive\":false,\"login\":\"\",\"isVod\":true,\"vodID\":\"" + ID + "\",\"playerType\":\"embed\"}}"))["data"]["videoPlaybackAccessToken"];
// Get video streams
string[] response = (await client.DownloadStringTaskAsync($"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken["value"]}&nauthsig={videoAccessToken["signature"]}&allow_source=true&player=twitchweb")).Split("\n");
// Init streams list
List<Stream> streams = new List<Stream>();
// Parse response
for (int i = 2; i < response.Length; i += 3)
{
// Parse line 2
Match line2 = L2Regex.Match(response[i + 1]);
// Get info
Uri url = new Uri(response[i + 2]);
int width = int.Parse(line2.Groups["width"].Value);
int height = int.Parse(line2.Groups["height"].Value);
int frameRate = (int)Math.Round(double.Parse(line2.Groups["frame_rate"].Value));
string videoCodec = line2.Groups["video_codec"].Value;
string audioCodec = line2.Groups["audio_codec"].Value;
// Create stream
Stream stream = new Stream(url, true, StreamType.AudioVideo)
{
Width = width,
Height = height,
FrameRate = frameRate,
VideoCodec = videoCodec,
AudioCodec = audioCodec,
};
// Add stream
streams.Add(stream);
}
// Set Streams parameter
Streams = streams.ToArray();
}
// DOWNLOAD AND TRANSCODE VOD
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Invoke DownloadingStarted event
DownloadingStarted?.Invoke(this, EventArgs.Empty);
// Get video chunks
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(audioVideoStream.Url);
// Passive trim
if ((bool)Config.GetValue("twitch_vod_passive_trim"))
{
var trimResult = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration);
trimStart = trimResult.TrimStart;
trimEnd = trimResult.TrimEnd;
}
// Download
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
float chunksDownloaded = 0;
Task<byte[]> downloadTask;
Task writeTask;
downloadTask = DownloadChunkAsync(chunksList[0].ChunkUrl);
await downloadTask;
for (int i = 1; i < chunksList.Count; i++)
{
writeTask = WriteChunkToFileAsync(rawFile, downloadTask.Result);
downloadTask = DownloadChunkAsync(chunksList[i].ChunkUrl);
await Task.WhenAll(writeTask, downloadTask);
DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null));
}
await WriteChunkToFileAsync(rawFile, downloadTask.Result);
DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null));
DownloadingCompleted?.Invoke(this, EventArgs.Empty);
// Processing
StorageFile outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd);
mediaProcessor.ProcessingStarted += ProcessingStarted;
mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged;
mediaProcessor.ProcessingCompleted += ProcessingCompleted;
await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken);
// Return output file
return outputFile;
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioVideoStream, extension, mediaType, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
#endregion
#region LOCAL METHODS
// GET CHUNKS DATA FROM M3U8 PLAYLIST
private static async Task<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> ExtractChunksFromM3U8Async(Uri streamUrl)
{
// Create client
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
// Get playlist
string response = await client.DownloadStringTaskAsync(streamUrl);
Debug.WriteLine(response);
// Create dictionary
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>();
// Pack data into dictionary
foreach (Match chunk in ChunkRegex.Matches(response))
{
Uri chunkUrl = new Uri($"{streamUrl.AbsoluteUri.Replace(System.IO.Path.GetFileName(streamUrl.AbsoluteUri), "")}{chunk.Groups["filename"].Value}");
TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value));
chunks.Add((chunkUrl, chunkDuration));
}
// Return chunks data
return chunks;
}
// PASSIVE TRIM
private static (TimeSpan TrimStart, TimeSpan TrimEnd) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration)
{
// Copy duration
TimeSpan newDuration = duration;
// Trim at start
while (chunksList[0].ChunkDuration <= trimStart)
{
trimStart = trimStart.Subtract(chunksList[0].ChunkDuration);
trimEnd = trimEnd.Subtract(chunksList[0].ChunkDuration);
newDuration = newDuration.Subtract(chunksList[0].ChunkDuration);
chunksList.RemoveAt(0);
}
// Trim at end
while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trimEnd))
{
newDuration = newDuration.Subtract(chunksList.Last().ChunkDuration);
chunksList.RemoveAt(chunksList.Count - 1);
}
// Return data
return (trimStart, trimEnd);
}
// DOWNLOAD CHUNK
private static async Task<byte[]> DownloadChunkAsync(Uri chunkUrl)
{
int retriesCount = 0;
while ((bool)Config.GetValue("twitch_vod_downloading_chunk_retry_after_error") && retriesCount < (int)Config.GetValue("twitch_vod_downloading_chunk_max_retries"))
{
try
{
using (WebClient client = new WebClient())
{
return await client.DownloadDataTaskAsync(chunkUrl);
}
}
catch
{
retriesCount++;
await Task.Delay((int)Config.GetValue("twitch_vod_downloading_chunk_retries_delay"));
}
}
throw new WebException("An error occurs while downloading a Twitch VOD chunk");
}
// WRITE CHUNK TO FILE
private static Task WriteChunkToFileAsync(StorageFile file, byte[] chunk)
{
return Task.Factory.StartNew(() =>
{
using (var stream = new System.IO.FileStream(file.Path, System.IO.FileMode.Append))
{
stream.Write(chunk, 0, chunk.Length);
stream.Close();
}
});
}
#endregion
#region EVENT HANDLERS
public event EventHandler DownloadingStarted;
public event EventHandler<ProgressChangedEventArgs> DownloadingProgressChanged;
public event EventHandler DownloadingCompleted;
public event EventHandler ProcessingStarted;
public event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
public event EventHandler ProcessingCompleted;
#endregion
}
}