From 365330f1d90416a26d04081767168466529869da Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 26 Jan 2023 23:48:12 +0100 Subject: [PATCH] Videos: Create some base classes for streams --- src/invidious/videos/streams.cr | 267 ++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 src/invidious/videos/streams.cr diff --git a/src/invidious/videos/streams.cr b/src/invidious/videos/streams.cr new file mode 100644 index 00000000..9cd0eb69 --- /dev/null +++ b/src/invidious/videos/streams.cr @@ -0,0 +1,267 @@ +module Invidious::Videos + # ------------------ + # Structs & Enums + # ------------------ + + # "AUDIO_QUALITY_" + enum AudioQuality + Low + Medium + end + + enum ProjType + Rectangular + Equirectangular + Mesh + end + + struct ByteRange + getter start : UInt32 + getter end : UInt32 + + def initialize(@start, @end) + end + + def to_s + return "#{@start}-#{@end}" + end + end + + # ------------------ + # Traits + # ------------------ + + # Properties common to all streams containing audio + module AudioProperties + macro included + property audio_quality : AudioQuality + property audio_sample_rate : UInt32 + property audio_channels : UInt8 + property audio_loudness_db : Float64 = 0.0 + + private macro init_audio_properties(format) + @audio_quality = AudioQuality.parse(format["audioQuality"].as_s.lstrip("AUDIO_QUALITY_")) + @audio_sample_rate = format["audioSampleRate"].as_s.to_u32 + @audio_channels = format["audioChannels"].as_i.to_u8 + @audio_loudness_db = format["loudnessDb"]?.try &.as_f || 0.0 + end + end + end + + # Properties common to all streams containing video + module VideoProperties + macro included + property video_width : UInt32 + property video_height : UInt32 + property video_fps : UInt16 + + private macro init_video_properties(format) + @video_width = format["width"].as_i.to_u32 + @video_height = format["height"].as_i.to_u32 + @video_fps = format["fps"].as_i.to_u16 + end + end + end + + # Properties common to all audio & video streams + module AVCommonProperties + macro included + property bitrate : UInt64 + property bitrate_avg : UInt64? + property content_length : UInt64 + + private macro init_av_common_properties(format) + @bitrate = format["bitrate"].as_i.to_u64 + @bitrate_avg = format["averageBitrate"]?.try &.as_i.to_u64 + @content_length = format["contentLength"].as_s.to_u64 + end + end + end + + # Properties that only applies to mulit-lingual adaptative streams. + # They apply to audio and text streams (notably text/mp4). + # + # Sample JSON for an audio track: + # "audioTrack": { + # "displayName": "Arabic", + # "id": "ar.0", + # "audioIsDefault": false + # }, + # + # Sample JSON for a caption track: + # "captionTrack": { + # "displayName": "English", + # "vssId": ".en.eEY6OEpapPo", + # "languageCode": "en" + # } + module TrackProperties + macro included + property track_id : String? + property track_name : String = "default" + property iso_code : String? + property default : Bool = false + + private macro init_track_properties(format) + if audio_track = format["audioTrack"]? + id = audio_track["id"].as_s + + @track_id = id + @track_name = audio_track["displayName"].as_s + + @iso_code = id.gsub(".0", "") + @default = audio_track["audioIsDefault"].as_bool + # + elsif caption_track = format["captionTrack"]? + @track_name = caption_track["displayName"].as_s + @track_id = caption_track["vssId"].as_s + @iso_code = caption_track["languageCode"].as_s + end + end + end + end + + # Properties that only apply to adaptative streams of regular videos + module AdaptativeProperties + macro included + property init_range : ByteRange? + property index_range : ByteRange? + + private macro init_adaptative_properties(format) + if init_range = format["initRange"]? + @init_range = ByteRange.new( + init_range["start"].as_i.to_u32, + init_range["end"].as_i.to_u32 + ) + end + + if index_range = format["indexRange"]? + @index_range = ByteRange.new( + index_range["start"].as_i.to_u32, + index_range["end"].as_i.to_u32 + ) + end + end + end + end + + # Properties that only apply to adaptative streams from livestrams + # (either in progress, or recenlty ended) + module LiveProperties + macro included + property target_duration : UInt32? + property max_dvr_duration : UInt32? + + private macro init_live_properties(format) + @target_duration = format["targetDurationSec"]?.try(&.as_i.to_u32) + @max_dvr_duration = format["maxDvrDurationSec"]?.try(&.as_i.to_u32) + end + end + end + + # ------------------ + # Base class + # ------------------ + + # Base stream class defining all the common properties for all streams + abstract class Stream + getter itag : UInt16 + getter label : String + property url : String + + getter raw_mime_type : String + getter mime_type : String + getter codecs : String + + getter last_modified : Time? + + getter projection_type : ProjType + + def initialize(format : JSON::Any, @label) + @itag = format["itag"].as_i.to_u16 + @url = format["url"].as_s + + @raw_mime_type = format["mimeType"].as_s + + # Extract MIME type and codecs from the raw mimeType string + @mime_type, raw_codecs = @raw_mime_type.split(';') + @codecs = raw_codecs.lstrip(" codecs=\"").rstrip('"') + + # Last modified is not present on livestreams + if last_modified = format["lastModified"].as_s + # E.g "1670664306(.)849305" + # Note: (.) is not present in the input data, it's used here to show + # the demarcation between seconds and microseconds. + timestamp = last_modified[0...10] + microseconds = last_modified[10..] + + @last_modified = Time.utc( + seconds: timestamp.to_i64, + nanoseconds: microseconds.to_i * 1000 + ) + end + + @projection_type = ProjType.parse(format["projectionType"].as_s) + + # Initialize extra properties as required + {% begin %} + {% + properties_types = [ + AudioProperties, + VideoProperties, + TrackProperties, + AVCommonProperties, + AdaptativeProperties, + LiveProperties, + ] + %} + + {% for type in properties_types %} + # Call the appropriate initialization macro if self + # inherits from the given type + {% if @type < type %} + init_{{type.id.split("::").last.id.underscore}}(format) + {% end %} + {% end %} + {% end %} + end + end + + # ------------------ + # Children classes + # ------------------ + + # An HTTP progressive stream (audio + video) + class ProgressiveHttpStream < Stream + include AudioProperties + include VideoProperties + include AVCommonProperties + end + + # Base class for adaptative (DASH) streams + abstract class AdaptativeStream < Stream + include AdaptativeProperties + include LiveProperties + end + + # An audio-only adaptative (DASH) stream + class AdaptativeAudioStream < AdaptativeStream + include AudioProperties + include AVCommonProperties + end + + # An audio-only adaptative (DASH) stream with track informations + class AdaptativeAudioTrackStream < AdaptativeAudioStream + include TrackProperties + end + + # A video-only adaptative (DASH) stream + class AdaptativeVideoStream < AdaptativeStream + include VideoProperties + include AVCommonProperties + end + + # A text-only adaptative (DASH) stream + class AdaptativeTextStream < AdaptativeStream + include TrackProperties + end +end