/*
* 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
}
}