/* * VideoKit * Copyright © 2026 Yusuf Olokoba. All Rights Reserved. */ #nullable enable namespace VideoKit { using AOT; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using Muna; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NJsonSchema; using NJsonSchema.Generation; using Internal; using static Muna.Beta.OpenAI.SpeechService; using BinaryData = Muna.Beta.OpenAI.BinaryData; using Status = Internal.VideoKit.Status; /// /// Media asset. /// public sealed class MediaAsset { #region --Enumerations-- /// /// Media type. /// public enum MediaType : int { // CHECK // `VideoKit.h` /// /// Unknown or unsupported media type. /// [EnumMember(Value = @"unknown")] Unknown = 0, /// /// Image. /// [EnumMember(Value = @"image")] Image = 1, /// /// Audio. /// [EnumMember(Value = @"audio")] Audio = 2, /// /// Video. /// [EnumMember(Value = @"video")] Video = 3, /// /// Text. /// [EnumMember(Value = @"text")] Text = 4, /// /// Sequence. /// [EnumMember(Value = @"sequence")] Sequence = 5, } /// /// Audio narration voice. /// [JsonConverter(typeof(StringEnumConverter))] public enum NarrationVoice : int { /// /// Male 1 narration voice. /// [EnumMember(Value = @"kevin")] Kevin = 1, /// /// Male 2 narration voice. /// [EnumMember(Value = @"arjun")] Arjun = 2, /// /// Male 3 narration voice. /// [EnumMember(Value = @"dami")] Dami = 3, /// /// Male 4 narration voice. /// [EnumMember(Value = @"juan")] Juan = 4, /// /// Female 1 narration voice. /// [EnumMember(Value = @"rhea")] Rhea = 5, /// /// Female 2 narration voice. /// [EnumMember(Value = @"aliyah")] Aliyah = 6, /// /// Female 3 narration voice. /// [EnumMember(Value = @"kristen")] Kristen = 7, /// /// Female 4 narration voice. /// [EnumMember(Value = @"salma")] Salma = 8, } #endregion #region --Properties-- /// /// Path to media asset. /// This is `null` for sequence assets. /// public string? path { get { var sb = new StringBuilder(2048); var status = handle.GetMediaAssetPath(sb, sb.Capacity); return status == Status.Ok ? sb.ToString() : null; } } /// /// Asset media type. /// public MediaType type => handle.GetMediaAssetMediaType(out var type).Throw() == Status.Ok ? type : default; /// /// Image or video width. /// public int width => handle.GetMediaAssetWidth(out var width) == Status.Ok ? width : default; /// /// Image or video height. /// public int height => handle.GetMediaAssetHeight(out var height) == Status.Ok ? height : default; /// /// Video frame rate. /// public float frameRate => handle.GetMediaAssetFrameRate(out var frameRate) == Status.Ok ? frameRate : default; /// /// Audio sample rate. /// public int sampleRate => handle.GetMediaAssetSampleRate(out var sampleRate) == Status.Ok ? sampleRate : default; /// /// Audio channel count. /// public int channelCount => handle.GetMediaAssetChannelCount(out var channelCount) == Status.Ok ? channelCount : default; /// /// Video or audio duration in seconds. /// public float duration => handle.GetMediaAssetDuration(out var duration) == Status.Ok ? duration : default; /// /// Media assets contained within this asset. /// This is only populated for `Sequence` assets. /// public IReadOnlyList assets => new NativeMediaSequence(this); #endregion #region --Creators-- /// /// Create a media aseet from a file. /// /// Path to media file. /// Media asset. public static Task FromFile(string path) { var tcs = new TaskCompletionSource(); var handle = GCHandle.Alloc(tcs, GCHandleType.Normal); try { VideoKit.CreateMediaAsset(path, OnCreateAsset, (IntPtr)handle).Throw(); } catch (Exception ex) { handle.Free(); tcs.SetException(ex); } return tcs.Task; } /// /// Create a media aseet from a texture. /// /// Texture. /// Image asset. public static Task FromTexture(Texture2D texture) { // Check if (texture == null) return Task.FromException(new ArgumentNullException(nameof(texture))); // Check if (!texture.isReadable) return Task.FromException(new ArgumentException(@"Cannot create media asset from texture that is not readable")); // Write to file var encoded = texture.EncodeToPNG(); var name = Guid.NewGuid().ToString("N"); var path = Path.Combine(Application.temporaryCachePath, $"{name}.png"); File.WriteAllBytes(path, encoded); // Create asset return FromFile(path); } /// /// Create a media aseet from an audio clip. /// NOTE: This requires an active VideoKit plan. /// /// Audio clip. /// Media format used to encode the audio. /// Audio asset. public static async Task FromAudioClip( AudioClip clip, MediaRecorder.Format format = MediaRecorder.Format.WAV ) { // Create audio buffer var sampleBuffer = new float[clip.samples * clip.channels]; clip.GetData(sampleBuffer, 0); using var audioBuffer = new AudioBuffer(clip.frequency, clip.channels, sampleBuffer); // Create asset var recorder = await MediaRecorder.Create( format, sampleRate: audioBuffer.sampleRate, channelCount: audioBuffer.channelCount ); recorder.Append(audioBuffer); var asset = await recorder.FinishWriting(); // Return return asset; } /// /// Create a media aseet from plain text. /// /// Text. /// Text asset. public static Task FromText(string text) { // Write to file var name = Guid.NewGuid().ToString("N"); var path = Path.Combine(Application.temporaryCachePath, $"{name}.txt"); File.WriteAllText(path, text); // Create asset return FromFile(path); } /// /// Create a media asset by prompting the user to select an image or video from the camera roll. /// NOTE: This requires iOS 14+. /// /// Desired asset type. /// Media asset. public static Task FromCameraRoll(MediaType type) { var tcs = new TaskCompletionSource(); var handle = GCHandle.Alloc(tcs, GCHandleType.Normal); try { VideoKit.CreateMediaAssetFromCameraRoll(type, OnCreateAsset, (IntPtr)handle).Throw(); } catch (Exception ex) { handle.Free(); tcs.SetException(ex); } return tcs.Task; } /// /// Create a media asset from a file in `StreamingAssets`. /// /// Relative file path in `StreamingAssets`. /// Media asset. public static async Task FromStreamingAssets(string path) { // Get absolute path var absolutePath = await StreamingAssetsToAbsolutePath(path); if (absolutePath == null) throw new InvalidOperationException($"Failed to create media asset because file '{path}' could not be found in `StreamingAssets`"); // Create asset return await FromFile(absolutePath); } /// /// Create a media asset by concatenating a set of media assets. /// /// Media assets to concatenate. /// Concatenated media asset. public static Task FromConcatenatingAssets(params MediaAsset[] assets) => FromConcatenatingAssets( assets, format: MediaRecorder.Format.MP4 ); /// /// Create a media asset by concatenating a set of media assets. /// /// Media assets to concatenate. /// Destination format for concatenated media asset. /// Subdirectory name to save recordings. This will be created if it does not exist. /// Concatenated media asset. public static async Task FromConcatenatingAssets( MediaAsset[] assets, MediaRecorder.Format format, string? prefix = null ) { // Check if (assets.Length == 0) throw new ArgumentException(@"Concatenate requires at least one media asset"); // Check all videos if (assets.Any(asset => asset.type != MediaType.Video)) throw new NotImplementedException(@"Concatenate only supports video assets"); // Check resolution var width = assets[0].width; var height = assets[0].height; if (assets.Any(asset => asset.width != width || asset.height != height)) throw new ArgumentException(@"Concatenate requires that all videos have the same resolution"); // Check no audio if (assets.Any(asset => asset.sampleRate > 0 && asset.channelCount > 0)) throw new NotImplementedException(@"Concatenate only supports videos without audio"); // Create destination recorder var frameRate = assets[0].frameRate; var recorder = await MediaRecorder.Create( format: format, width: width, height: height, frameRate: frameRate, sampleRate: 0, channelCount: 0, prefix: prefix ); // Append frames var data = new byte[width * height * 4]; var handle = GCHandle.Alloc(data, GCHandleType.Pinned); var dataPtr = handle.AddrOfPinnedObject(); var timebase = 0L; try { foreach (var asset in assets) { var lastTimestamp = 0L; foreach (var srcBuffer in asset.Read()) { using var dstBuffer = Wrap( dataPtr, width, height, timebase + srcBuffer.timestamp ); srcBuffer.CopyTo(dstBuffer); recorder.Append(dstBuffer); lastTimestamp = dstBuffer.timestamp; } timebase = lastTimestamp + (long)(1e+9f / frameRate); } } finally { handle.Free(); } // Finish return await recorder.FinishWriting(); // Helper static unsafe PixelBuffer Wrap(IntPtr handle, int width, int height, long timestamp) => new( width: width, height: height, format: PixelBuffer.Format.RGBA8888, data: (byte*)handle, timestamp: timestamp ); } /// /// Create a media asset by performing text-to-speech on the provided text prompt. /// /// Text to synthesize speech from. /// Voice to use for generation. See https://videokit.ai/reference/mediaasset for more information. /// Audio speed of the generated speech. /// Prediction acceleration. /// Generated audio asset. internal static async Task FromGeneratedSpeech( // INCOMPLETE string prompt, NarrationVoice voice, float speed = 1f, string? acceleration = default ) { var openai = VideoKitClient.Instance!.muna.Beta.OpenAI; var tag = SpeechPredictorMap[voice]; var speech = await openai.Audio.Speech.Create( model: tag, input: prompt, voice: GetEnumValueString(voice)!, speed: speed, responseFormat: ResponseFormat.PCM, acceleration: acceleration ); var clip = ToAudioClip(speech); var asset = await FromAudioClip(clip); return asset; } /// /// Create a media asset by transcribing on the provided audio clip. /// /// Audio clip to transcribe. /// Transcribed text asset. public static async Task FromGeneratedTranscription(AudioClip audio) { var audioAsset = await FromAudioClip(audio, MediaRecorder.Format.WAV); var transcriptionAsset = await FromGeneratedTranscription(audioAsset.path); return transcriptionAsset; } /// /// Create a media asset by transcribing on the provided audio clip. /// /// Path to audio file. /// Prediction acceleration. /// Transcribed text asset. public static async Task FromGeneratedTranscription( string path, string? acceleration = default ) { var openai = VideoKitClient.Instance!.muna.Beta.OpenAI; using var stream = File.OpenRead(path); var transcription = await openai.Audio.Transcriptions.Create( model: TranscribeTag, file: stream, acceleration: acceleration ); var asset = await FromText(transcription.Text); return asset; } /// /// Create a media asset by performing text-to-image on the provided text prompt. /// /// Text prompt to use to generate the image. /// Desired image width. NOTE: The generated image is not guaranteed to have this width. /// Desired image height. NOTE: The generated image is not guaranteed to have this height. /// Generated image asset. internal static async Task FromGeneratedImage( // INCOMPLETE string prompt, int width = 1024, int height = 1024 ) { return default; } #endregion #region --Converters-- /// /// Read text from the media asset. /// This can only be used on text assets. /// public string ToText() { // Check type if (type != MediaType.Text) throw new ArgumentException(@"`MediaAsset.ToText` can only be used on text assets"); // Check path if (string.IsNullOrEmpty(path)) throw new InvalidOperationException("Text asset does not have a valid file path"); // Read text return File.ReadAllText(path); } /// /// Create a texture from the media asset. /// This can only be used on image and video assets. /// /// Time to extract the texture from. This is only supported for video assets. /// Texture. public async Task ToTexture(float time = 0f) { // Check video if (type == MediaType.Video) throw new NotImplementedException(@"`MediaAsset.ToTexture` is not yet supported for video assets"); // Check type if (type != MediaType.Image) throw new ArgumentException(@"`MediaAsset.ToTexture` can only be used on image assets"); // Load data var uri = path![0] == '/' ? $"file://{path}" : path; using var request = UnityWebRequestTexture.GetTexture(uri); request.SendWebRequest(); while (!request.isDone) await Task.Yield(); // Check if (request.result != UnityWebRequest.Result.Success) throw new InvalidOperationException($"Image asset could not be loaded with error: {request.error}"); // Return return DownloadHandlerTexture.GetContent(request); } /// /// Create an audio clip from the media asset. /// /// Audio clip. public async Task ToAudioClip() { // CHECK // WAV only for now // Check type if (type != MediaType.Audio) throw new ArgumentException($"Cannot create audio clip from asset because asset has invalid type: {type}"); // Load data var uri = path![0] == '/' ? $"file://{path}" : path; using var request = UnityWebRequestMultimedia.GetAudioClip(uri, AudioType.WAV); request.SendWebRequest(); while (!request.isDone) await Task.Yield(); // Check if (request.result != UnityWebRequest.Result.Success) throw new InvalidOperationException($"Audio clip could not be loaded with error: {request.error}"); // Return return DownloadHandlerAudioClip.GetContent(request); } #endregion #region --Reading-- /// /// Read sample buffers in the media asset. /// /// Sample buffers in the media asset. public IEnumerable Read() where T : struct { var type = GetMediaType(); foreach (var sampleBuffer in Read(type)) { if (type == MediaType.Video) yield return (T)(object)new PixelBuffer(sampleBuffer); // sexy code is worth boxing overhead :p else if (type == MediaType.Audio) yield return (T)(object)new AudioBuffer(sampleBuffer); else break; } } /// /// Parse the text asset into a structure. /// /// Structure to parse into. /// Parsed structure. internal async Task Parse() { // Check if (type != MediaType.Text) throw new ArgumentException($"Cannot perform structured parsing on media asset because asset is not a text asset"); // Generate schema var settings = new JsonSchemaGeneratorSettings { GenerateAbstractSchemas = false, GenerateExamples = false, UseXmlDocumentation = false, ResolveExternalXmlDocumentation = false, FlattenInheritanceHierarchy = false, }; var schema = JsonSchema.FromType(settings); // Parse return default; } /// /// Take a sub-range of a media asset. /// /// Duration in seconds. /// Destination format for result media asset. /// Subdirectory name to save recordings. This will be created if it does not exist. /// Result media asset. public Task Take( float duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) => Take( duration: TimeSpan.FromSeconds(duration), format: format, prefix: prefix ); /// /// Take a sub-range of a media asset. /// /// Duration. /// Destination format for result media asset. /// Subdirectory name to save recordings. This will be created if it does not exist. /// Result media asset. public async Task Take( TimeSpan duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) { // Check video if (type != MediaType.Video) throw new NotImplementedException(@"Trimming media assets is only supported for videos"); // Check audio if (sampleRate > 0 && channelCount > 0) throw new NotImplementedException(@"Trimming videos with audio is not yet supported"); // Check asset duration if (this.duration < duration.TotalSeconds) return this; // Create recorder var recorder = await MediaRecorder.Create( format: format, width: width, height: height, frameRate: frameRate, sampleRate: 0, channelCount: 0, prefix: prefix ); // Copy var data = new byte[width * height * 4]; var handle = GCHandle.Alloc(data, GCHandleType.Pinned); var dataPtr = handle.AddrOfPinnedObject(); try { foreach (var srcBuffer in Read()) { var timestamp = srcBuffer.timestamp; if (timestamp > duration.TotalMilliseconds * 1e+6f) break; using var dstBuffer = Wrap( dataPtr, width, height, srcBuffer.timestamp ); srcBuffer.CopyTo(dstBuffer); recorder.Append(dstBuffer); } } finally { handle.Free(); } // Finish return await recorder.FinishWriting(); // Helper static unsafe PixelBuffer Wrap(IntPtr handle, int width, int height, long timestamp) => new( width: width, height: height, format: PixelBuffer.Format.RGBA8888, data: (byte*)handle, timestamp: timestamp ); } /// /// Take a sub-range of a media asset from the end. /// /// Duration in seconds. /// Destination format for result media asset. /// Subdirectory name to save recordings. This will be created if it does not exist. /// Result media asset. public Task TakeLast( float duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) => TakeLast( duration: TimeSpan.FromSeconds(duration), format: format, prefix: prefix ); /// /// Take a sub-range of a media asset. /// /// Duration in seconds. /// Destination format for result media asset. /// Subdirectory name to save recordings. This will be created if it does not exist. /// Result media asset. public async Task TakeLast( TimeSpan duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) { // Check video if (type != MediaType.Video) throw new NotImplementedException(@"Trimming media assets is only supported for videos"); // Check audio if (sampleRate > 0 && channelCount > 0) throw new NotImplementedException(@"Trimming videos with audio is not yet supported"); // Check asset duration if (this.duration < duration.TotalSeconds) return this; // Create recorder var recorder = await MediaRecorder.Create( format: format, width: width, height: height, frameRate: frameRate, sampleRate: 0, channelCount: 0, prefix: prefix ); // Copy var data = new byte[width * height * 4]; var handle = GCHandle.Alloc(data, GCHandleType.Pinned); var dataPtr = handle.AddrOfPinnedObject(); var durationNs = (long)(duration.TotalMilliseconds * 1e+6f); var assetDurationNs = (long)(this.duration * 1e+9f); var startTimeNs = assetDurationNs - durationNs; long? timebaseNs = null; try { foreach (var srcBuffer in Read()) { var timestamp = srcBuffer.timestamp; if (timestamp < startTimeNs) continue; if (timebaseNs == null) timebaseNs = timestamp; using var dstBuffer = Wrap( dataPtr, width, height, timestamp - timebaseNs.Value ); srcBuffer.CopyTo(dstBuffer); recorder.Append(dstBuffer); } } finally { handle.Free(); } // Finish return await recorder.FinishWriting(); // Helper static unsafe PixelBuffer Wrap(IntPtr handle, int width, int height, long timestamp) => new( width: width, height: height, format: PixelBuffer.Format.RGBA8888, data: (byte*)handle, timestamp: timestamp ); } #endregion #region --Sharing-- /// /// Share the media asset using the native sharing UI. /// /// Optional message to share with the media asset. /// Receiving app bundle ID or `null` if the user did not complete the share action. public Task Share(string? message = null) { // Check if (type == MediaType.Sequence) throw new InvalidOperationException(@"Sequence assets cannot be shared"); // Share var tcs = new TaskCompletionSource(); var handle = GCHandle.Alloc(tcs, GCHandleType.Normal); try { this.handle.ShareMediaAsset( message, OnShare, (IntPtr)handle ).Throw(); } catch (NotImplementedException) { tcs.SetResult(null); handle.Free(); } catch (Exception ex) { tcs.SetException(ex); handle.Free(); } // Return return tcs.Task; } /// /// Save the media asset to the camera roll. /// /// Optional album to save media asset to. /// Whether the asset was successfully saved to the camera roll. public Task SaveToCameraRoll(string? album = null) { // Check if (type == MediaType.Sequence) throw new InvalidOperationException(@"Sequence assets cannot be saved to the camera roll"); // Save var tcs = new TaskCompletionSource(); var handle = GCHandle.Alloc(tcs, GCHandleType.Normal); try { this.handle.SaveMediaAssetToCameraRoll( album, OnSaveToCameraRoll, (IntPtr)handle ).Throw(); } catch (NotImplementedException) { tcs.SetResult(false); handle.Free(); } catch (Exception ex) { tcs.SetException(ex); handle.Free(); } // Return return tcs.Task; } #endregion #region --Operations-- private readonly IntPtr handle; private readonly MediaAsset? parent; private static readonly Dictionary SpeechPredictorMap = new() { // INCOMPLETE }; internal const string TranscribeTag = @"@videokit/transcribe-v1"; internal MediaAsset(IntPtr handle, MediaAsset? parent = null) { this.handle = handle; this.parent = parent; } ~MediaAsset() { if (parent == null) handle.ReleaseMediaAsset(); } private IEnumerable Read(MediaType type) { handle.CreateMediaReader(type, out var reader).Throw(); try { for (;;) { var status = reader.ReadNextSampleBuffer(out var sampleBuffer); if (status == Status.InvalidOperation) break; if (sampleBuffer == default) continue; yield return sampleBuffer; sampleBuffer.ReleaseSampleBuffer(); } } finally { reader.ReleaseMediaReader(); } } public static implicit operator IntPtr(MediaAsset asset) => asset.handle; #endregion #region --Callbacks-- [MonoPInvokeCallback(typeof(VideoKit.MediaAssetHandler))] private static void OnCreateAsset(IntPtr context, IntPtr asset) { try { if (!VideoKit.IsAppDomainLoaded) return; var handle = (GCHandle)context; var tcs = handle.Target as TaskCompletionSource; handle.Free(); tcs?.SetResult(asset != IntPtr.Zero ? new MediaAsset(asset) : null); } catch (Exception ex) { Debug.LogException(ex); } } [MonoPInvokeCallback(typeof(VideoKit.MediaAssetShareHandler))] private static void OnShare(IntPtr context, IntPtr receiver) { try { // Check if (!VideoKit.IsAppDomainLoaded) return; // Get tcs var handle = (GCHandle)context; var tcs = handle.Target as TaskCompletionSource; handle.Free(); // Complete task var result = Marshal.PtrToStringUTF8(receiver); tcs?.SetResult(result); } catch (Exception ex) { Debug.LogException(ex); } } [MonoPInvokeCallback(typeof(VideoKit.MediaAssetShareHandler))] private static void OnSaveToCameraRoll(IntPtr context, IntPtr receiver) { try { // Check if (!VideoKit.IsAppDomainLoaded) return; // Get tcs var handle = (GCHandle)context; var tcs = handle.Target as TaskCompletionSource; handle.Free(); // Complete task var result = receiver != IntPtr.Zero; tcs?.SetResult(result); } catch (Exception ex) { Debug.LogException(ex); } } #endregion #region --Utilities-- private static async Task StreamingAssetsToAbsolutePath(string relativePath) { var fullPath = Path.Combine(Application.streamingAssetsPath, relativePath); if (Application.platform != RuntimePlatform.Android) return File.Exists(fullPath) ? fullPath : null; var persistentPath = Path.Combine(Application.persistentDataPath, relativePath); if (File.Exists(persistentPath)) return persistentPath; var directory = Path.GetDirectoryName(persistentPath); Directory.CreateDirectory(directory); using var request = UnityWebRequest.Get(fullPath); request.SendWebRequest(); while (!request.isDone) await Task.Yield(); if (request.result != UnityWebRequest.Result.Success) return null; File.WriteAllBytes(persistentPath, request.downloadHandler.data); return persistentPath; } private static MediaType GetMediaType() => typeof(T) switch { var x when x == typeof(AudioBuffer) => MediaType.Audio, var x when x == typeof(PixelBuffer) => MediaType.Video, _ => MediaType.Unknown, }; private static string? GetEnumValueString(Enum value) { var fieldInfo = value.GetType().GetField(value.ToString()); var attribute = fieldInfo? .GetCustomAttributes(typeof(EnumMemberAttribute), false)? .FirstOrDefault() as EnumMemberAttribute; return (attribute?.IsValueSetExplicitly ?? false) ? attribute.Value : null; } private static unsafe AudioClip ToAudioClip(BinaryData data) { // Match sample rate and channel count var rateMatch = Regex.Match(data.MediaType, @"rate=(\d+)"); var channelsMatch = Regex.Match(data.MediaType, @"channels=(\d+)"); if (!rateMatch.Success || !channelsMatch.Success) throw new ArgumentException($"Failed to extract audio format from speech binary data because media type is invalid: '{data.MediaType}'"); // Parse if (!int.TryParse(rateMatch.Groups[1].Value, out var sampleRate)) throw new ArgumentException($"Failed to parse sample rate from speech binary data because it is invalid: '{rateMatch.Value}'"); if (!int.TryParse(channelsMatch.Groups[1].Value, out var channelCount)) throw new ArgumentException($"Failed to parse channel count from speech binary data because it is invalid: '{channelsMatch.Value}'"); // Create clip var sampleCount = data.Length / sizeof(float); var frameCount = sampleCount / channelCount; var clip = AudioClip.Create( "audio", lengthSamples: frameCount, channels: channelCount, frequency: sampleRate, stream: false ); // Copy data var samples = new float[sampleCount]; Buffer.BlockCopy(data.ToArray(), 0, samples, 0, data.Length); clip.SetData(samples, 0); // Return return clip; } private readonly struct NativeMediaSequence : IReadOnlyList { private readonly MediaAsset asset; public NativeMediaSequence(MediaAsset asset) => this.asset = asset; public int Count { get { if (asset.handle.GetMediaAssetSubAssetCount(out var count) == Status.Ok) return count; return default; } } public MediaAsset? this[int index] { get { asset.handle.GetMediaAssetSubAsset(index, out var subAsset).Throw(); return new MediaAsset(subAsset, parent: asset); } } IEnumerator IEnumerable.GetEnumerator() { for (var idx = 0; idx < Count; ++idx) yield return this[idx]; } IEnumerator IEnumerable.GetEnumerator() => (this as IEnumerable).GetEnumerator(); } #endregion #region --Deprecated-- [Obsolete(@"Deprecated in VideoKit 1.0.11. Use `MediaAsset.FromConcatenatingAssets` static method instead.")] public static Task Concatenate(params MediaAsset[] assets) => FromConcatenatingAssets( assets, format: MediaRecorder.Format.MP4 ); [Obsolete(@"Deprecated in VideoKit 1.0.11. Use `MediaAsset.FromConcatenatingAssets` static method instead.")] public static Task Concatenate( MediaAsset[] assets, MediaRecorder.Format format, string? prefix = null ) => FromConcatenatingAssets(assets, format: format, prefix: prefix); [Obsolete(@"Deprecated in VideoKit 1.0.11. Use `MediaAsset.Take` instance method instead.")] public static Task Take( MediaAsset asset, float duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) => asset.Take(duration, format: format, prefix: prefix); [Obsolete(@"Deprecated in VideoKit 1.0.11. Use `MediaAsset.Take` instance method instead.")] public static Task Take( MediaAsset asset, TimeSpan duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) => asset.Take(duration, format: format, prefix: prefix); [Obsolete(@"Deprecated in VideoKit 1.0.11. Use `MediaAsset.TakeLast` instance method instead.")] public static Task TakeLast( MediaAsset asset, float duration, MediaRecorder.Format format = MediaRecorder.Format.MP4, string? prefix = null ) => asset.TakeLast(duration, format: format, prefix: prefix); #endregion } }