Skip to content
This repository has been archived by the owner on Mar 22, 2022. It is now read-only.

Add split audio track source #377

Merged
merged 7 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/TestAppUwp/MediaPlayerPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public string DisplayName
/// </summary>
public class AudioTrackViewModel
{
// FIXME - this leaks 'source', never disposed (and is the track itself disposed??)
public AudioTrackSource Source;
public IAudioTrack Track;
public MediaTrack TrackImpl;
public bool IsRemote;
Expand Down
9 changes: 7 additions & 2 deletions examples/TestAppUwp/ViewModel/AudioCaptureViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@ public async Task AddAudioTrackFromDeviceAsync(string trackName)

await RequestMediaAccessAsync(StreamingCaptureMode.Audio);

var settings = new LocalAudioTrackSettings
// FIXME - this leaks 'source', never disposed (and is the track itself disposed??)
var initConfig = new LocalAudioDeviceInitConfig();
var source = await AudioTrackSource.CreateFromDeviceAsync(initConfig);

var settings = new LocalAudioTrackInitConfig
{
trackName = trackName
};
var track = await LocalAudioTrack.CreateFromDeviceAsync(settings);
var track = await LocalAudioTrack.CreateFromSourceAsync(source, settings);

SessionModel.Current.AudioTracks.Add(new AudioTrackViewModel
{
Source = source,
Track = track,
TrackImpl = track,
IsRemote = false,
Expand Down
5 changes: 4 additions & 1 deletion examples/TestAppUwp/ViewModel/MediaPlayerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ public MediaPlayerViewModel()
DisplayName = "Local microphone (default device)",
Factory = async () =>
{
return await LocalAudioTrack.CreateFromDeviceAsync();
// FIXME - this leaks 'source', never disposed (and is the track itself disposed??)
var source = await AudioTrackSource.CreateFromDeviceAsync();
var settings = new LocalAudioTrackInitConfig();
return await LocalAudioTrack.CreateFromSourceAsync(source, settings);
}
});

Expand Down
13 changes: 12 additions & 1 deletion examples/TestNetCoreConsole/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ static async Task Main(string[] args)
{
Transceiver audioTransceiver = null;
Transceiver videoTransceiver = null;
AudioTrackSource audioTrackSource = null;
LocalAudioTrack localAudioTrack = null;
LocalVideoTrack localVideoTrack = null;

Expand Down Expand Up @@ -58,7 +59,13 @@ static async Task Main(string[] args)
if (needAudio)
{
Console.WriteLine("Opening local microphone...");
localAudioTrack = await LocalAudioTrack.CreateFromDeviceAsync();
audioTrackSource = await AudioTrackSource.CreateFromDeviceAsync();

Console.WriteLine("Create local audio track...");
var trackSettings = new LocalAudioTrackInitConfig { trackName = "mic_track" };
localAudioTrack = await LocalAudioTrack.CreateFromSourceAsync(audioTrackSource, trackSettings);

Console.WriteLine("Create audio transceiver and add mic track...");
audioTransceiver = pc.AddTransceiver(MediaKind.Audio);
audioTransceiver.DesiredDirection = Transceiver.Direction.SendReceive;
audioTransceiver.LocalAudioTrack = localAudioTrack;
Expand Down Expand Up @@ -118,6 +125,10 @@ static async Task Main(string[] args)
localVideoTrack?.Dispose();

Console.WriteLine("Program termined.");

localAudioTrack.Dispose();
localVideoTrack.Dispose();
audioTrackSource.Dispose();
}
}
}
180 changes: 180 additions & 0 deletions libs/Microsoft.MixedReality.WebRTC/AudioTrackSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.MixedReality.WebRTC.Interop;

namespace Microsoft.MixedReality.WebRTC
{
/// <summary>
/// Configuration to initialize capture on a local audio device (microphone).
/// </summary>
public class LocalAudioDeviceInitConfig
{
/// <summary>
/// Enable automated gain control (AGC) on the audio device capture pipeline.
/// </summary>
public bool? AutoGainControl = null;
}

/// <summary>
/// Audio source for WebRTC audio tracks.
///
/// The audio source is not bound to any peer connection, and can therefore be shared by multiple audio
/// tracks from different peer connections. This is especially useful to share local audio capture devices
/// (microphones) amongst multiple peer connections when building a multi-peer experience with a mesh topology
/// (one connection per pair of peers).
///
/// The user owns the audio track source, and is in charge of keeping it alive until after all tracks using it
/// are destroyed, and then dispose of it. The behavior of disposing of the track source while a track is still
/// using it is undefined. The <see cref="Tracks"/> property contains the list of tracks currently using the
/// source.
/// </summary>
/// <seealso cref="LocalAudioTrack"/>
public class AudioTrackSource : IDisposable
{
/// <summary>
/// A name for the audio track source, used for logging and debugging.
/// </summary>
public string Name
{
get
{
// Note: the name cannot change internally, so no need to query the native layer.
// This avoids a round-trip to native and some string encoding conversion.
return _name;
}
set
{
AudioTrackSourceInterop.AudioTrackSource_SetName(_nativeHandle, value);
_name = value;
}
}

/// <summary>
/// List of local audio tracks this source is providing raw audio frames to.
/// </summary>
public IReadOnlyList<LocalAudioTrack> Tracks => _tracks;

/// <summary>
/// Handle to the native AudioTrackSource object.
/// </summary>
/// <remarks>
/// In native land this is a <code>Microsoft::MixedReality::WebRTC::AudioTrackSourceHandle</code>.
/// </remarks>
internal AudioTrackSourceHandle _nativeHandle { get; private set; } = new AudioTrackSourceHandle();

/// <summary>
/// Backing field for <see cref="Name"/>, and cache for the native name.
/// Since the name can only be set by the user, this cached value is always up-to-date with the
/// internal name of the native object, by design.
/// </summary>
private string _name = string.Empty;

/// <summary>
/// Backing field for <see cref="Tracks"/>.
/// </summary>
private List<LocalAudioTrack> _tracks = new List<LocalAudioTrack>();

/// <summary>
/// Create an audio track source using a local audio capture device (microphone).
/// </summary>
/// <param name="initConfig">Optional configuration to initialize the audio capture on the device.</param>
/// <returns>The newly create audio track source.</returns>
public static Task<AudioTrackSource> CreateFromDeviceAsync(LocalAudioDeviceInitConfig initConfig = null)
{
return Task.Run(() =>
{
// On UWP this cannot be called from the main UI thread, so always call it from
// a background worker thread.

var config = new AudioTrackSourceInterop.LocalAudioDeviceMarshalInitConfig(initConfig);
uint ret = AudioTrackSourceInterop.AudioTrackSource_CreateFromDevice(in config, out AudioTrackSourceHandle handle);
Utils.ThrowOnErrorCode(ret);
return new AudioTrackSource(handle);
});
}

internal AudioTrackSource(AudioTrackSourceHandle nativeHandle)
{
_nativeHandle = nativeHandle;
}

/// <inheritdoc/>
public void Dispose()
{
if (_nativeHandle.IsClosed)
{
return;
}

// TODO - Can we support destroying the source and leaving tracks with silence instead?
if (_tracks.Count > 0)
{
throw new InvalidOperationException($"Trying to dispose of AudioTrackSource '{Name}' while still in use by one or more audio tracks.");
}

// Unregister from tracks
// TODO...
//AudioTrackSourceInterop.AudioTrackSource_Shutdown(_nativeHandle);

// Destroy the native object. This may be delayed if a P/Invoke callback is underway,
// but will be handled at some point anyway, even if the managed instance is gone.
_nativeHandle.Dispose();
}

/// <summary>
/// Internal callback when a track starts using this source.
/// </summary>
/// <param name="track">The track using this source.</param>
internal void OnTrackAddedToSource(LocalAudioTrack track)
{
Debug.Assert(!_nativeHandle.IsClosed);
Debug.Assert(!_tracks.Contains(track));
_tracks.Add(track);
}

/// <summary>
/// Internal callback when a track stops using this source.
/// </summary>
/// <param name="track">The track not using this source anymore.</param>
internal void OnTrackRemovedFromSource(LocalAudioTrack track)
{
Debug.Assert(!_nativeHandle.IsClosed);
bool removed = _tracks.Remove(track);
Debug.Assert(removed);
}

/// <summary>
/// Internal callback when a list of tracks stop using this source, generally
/// as a result of a peer connection owning said tracks being closed.
/// </summary>
/// <param name="tracks">The list of tracks not using this source anymore.</param>
internal void OnTracksRemovedFromSource(IEnumerable<LocalAudioTrack> tracks)
{
Debug.Assert(!_nativeHandle.IsClosed);
var remainingTracks = new List<LocalAudioTrack>();
foreach (var track in tracks)
{
if (track.Source == this)
{
Debug.Assert(_tracks.Contains(track));
}
else
{
remainingTracks.Add(track);
}
}
_tracks = remainingTracks;
}

/// <inheritdoc/>
public override string ToString()
{
return $"(AudioTrackSource)\"{Name}\"";
}
}
}
107 changes: 107 additions & 0 deletions libs/Microsoft.MixedReality.WebRTC/Interop/AudioTrackSourceInterop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace Microsoft.MixedReality.WebRTC.Interop
{
/// <summary>
/// Handle to a native audio track source object.
/// </summary>
internal sealed class AudioTrackSourceHandle : SafeHandle
{
/// <summary>
/// Check if the current handle is invalid, which means it is not referencing
/// an actual native object. Note that a valid handle only means that the internal
/// handle references a native object, but does not guarantee that the native
/// object is still accessible. It is only safe to access the native object if
/// the handle is not closed, which implies it being valid.
/// </summary>
public override bool IsInvalid => (handle == IntPtr.Zero);

/// <summary>
/// Default constructor for an invalid handle.
/// </summary>
public AudioTrackSourceHandle() : base(IntPtr.Zero, ownsHandle: true)
{
}

/// <summary>
/// Constructor for a valid handle referencing the given native object.
/// </summary>
/// <param name="handle">The valid internal handle to the native object.</param>
public AudioTrackSourceHandle(IntPtr handle) : base(IntPtr.Zero, ownsHandle: true)
{
SetHandle(handle);
}

/// <summary>
/// Release the native object while the handle is being closed.
/// </summary>
/// <returns>Return <c>true</c> if the native object was successfully released.</returns>
protected override bool ReleaseHandle()
{
AudioTrackSourceInterop.AudioTrackSource_RemoveRef(handle);
return true;
}
}

internal class AudioTrackSourceInterop
{
/// <summary>
/// Marshaling struct for initializing settings when opening a local audio device.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal ref struct LocalAudioDeviceMarshalInitConfig
{
public mrsOptBool AutoGainControl;

/// <summary>
/// Constructor for creating a local audio device initialization settings marshaling struct.
/// </summary>
/// <param name="settings">The settings to initialize the newly created marshaling struct.</param>
/// <seealso cref="AudioTrackSource.CreateFromDeviceAsync(LocalAudioDeviceInitConfig)"/>
public LocalAudioDeviceMarshalInitConfig(LocalAudioDeviceInitConfig settings)
{
AutoGainControl = (mrsOptBool)settings?.AutoGainControl;
}
}

#region P/Invoke static functions

[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceAddRef")]
public static unsafe extern void AudioTrackSource_AddRef(AudioTrackSourceHandle handle);

// Note - This is used during SafeHandle.ReleaseHandle(), so cannot use AudioTrackSourceHandle
[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceRemoveRef")]
public static unsafe extern void AudioTrackSource_RemoveRef(IntPtr handle);

[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceSetName")]
public static unsafe extern void AudioTrackSource_SetName(AudioTrackSourceHandle handle, string name);

[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceGetName")]
public static unsafe extern uint AudioTrackSource_GetName(AudioTrackSourceHandle handle, StringBuilder buffer,
ref ulong bufferCapacity);

[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceSetUserData")]
public static unsafe extern void AudioTrackSource_SetUserData(AudioTrackSourceHandle handle, IntPtr userData);

[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceGetUserData")]
public static unsafe extern IntPtr AudioTrackSource_GetUserData(AudioTrackSourceHandle handle);

[DllImport(Utils.dllPath, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi,
EntryPoint = "mrsAudioTrackSourceCreateFromDevice")]
public static unsafe extern uint AudioTrackSource_CreateFromDevice(
in LocalAudioDeviceMarshalInitConfig config, out AudioTrackSourceHandle sourceHandle);

#endregion
}
}
Loading