-
Notifications
You must be signed in to change notification settings - Fork 60
Expose Frame Packet Trailer (FrameMetadata) for video #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
31ce9d8
d041dab
08d39e4
e982fc1
cef0645
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using UnityEngine; | ||
|
|
||
| namespace LiveKit.PlayModeTests.Utils | ||
| { | ||
| /// <summary> | ||
| /// Empty MonoBehaviour used by tests to host long-running coroutines | ||
| /// (e.g. <see cref="RtcVideoSource.Update"/> and <see cref="VideoStream.Update"/>) | ||
| /// that the test itself cannot host because <c>[UnityTest]</c> bodies must yield | ||
| /// back to the test runner. | ||
| /// </summary> | ||
| public class CoroutineRunner : MonoBehaviour { } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| using LiveKit.Proto; | ||
| using Unity.Collections; | ||
|
|
||
| namespace LiveKit.PlayModeTests.Utils | ||
| { | ||
| /// <summary> | ||
| /// Test-only <see cref="RtcVideoSource"/> registered with FFI at a fixed | ||
| /// resolution. Two modes via <paramref name="pushFrames"/>: | ||
| /// <list type="bullet"> | ||
| /// <item><c>false</c> (default): never pushes frames. Use when the test only | ||
| /// needs the publication to propagate via signaling (e.g. | ||
| /// <see cref="RemoteTrackPublication"/> APIs that operate on metadata).</item> | ||
| /// <item><c>true</c>: pushes a continuous stream of zero-filled RGBA frames. | ||
| /// Use when the test needs actual media flow (e.g. validating per-frame | ||
| /// metadata round-trips through the FFI / RTP path).</item> | ||
| /// </list> | ||
| /// </summary> | ||
| public sealed class TestVideoSource : RtcVideoSource | ||
| { | ||
| private readonly int _width; | ||
| private readonly int _height; | ||
| private readonly bool _pushFrames; | ||
|
|
||
| public override int GetWidth() => _width; | ||
| public override int GetHeight() => _height; | ||
|
|
||
| protected override VideoRotation GetVideoRotation() => VideoRotation._0; | ||
|
|
||
| protected override bool ReadBuffer() | ||
| { | ||
| if (!_pushFrames) return false; | ||
|
|
||
| if (!_captureBuffer.IsCreated) | ||
| { | ||
| _captureBuffer = new NativeArray<byte>( | ||
| _width * _height * 4, | ||
| Allocator.Persistent, | ||
| NativeArrayOptions.ClearMemory); | ||
| } | ||
| _requestPending = true; | ||
| return false; | ||
| } | ||
|
|
||
| public TestVideoSource(bool pushFrames = false, int width = 16, int height = 16) | ||
| : base(VideoStreamSource.Texture, VideoBufferType.Rgba) | ||
| { | ||
| _width = width; | ||
| _height = height; | ||
| _pushFrames = pushFrames; | ||
| Init(); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| using System.Collections; | ||
| using LiveKit.PlayModeTests.Utils; | ||
| using LiveKit.Proto; | ||
| using NUnit.Framework; | ||
| using UnityEngine; | ||
| using UnityEngine.TestTools; | ||
|
|
||
| namespace LiveKit.PlayModeTests | ||
| { | ||
| public class VideoFrameMetadataTests | ||
| { | ||
| const string VideoTrackName = "metadata-video-track"; | ||
|
|
||
| static (TestRoomContext.ConnectionOptions publisher, TestRoomContext.ConnectionOptions subscriber) TwoPeers() | ||
| { | ||
| var publisher = TestRoomContext.ConnectionOptions.Default; | ||
| publisher.Identity = "metadata-publisher"; | ||
| var subscriber = TestRoomContext.ConnectionOptions.Default; | ||
| subscriber.Identity = "metadata-subscriber"; | ||
| return (publisher, subscriber); | ||
| } | ||
|
|
||
| [UnityTest, Category("E2E")] | ||
| public IEnumerator VideoFrame_AttachedMetadata_ReceivedOnSubscriber() | ||
| { | ||
| var (publisher, subscriber) = TwoPeers(); | ||
| using var context = new TestRoomContext(new[] { publisher, subscriber }); | ||
| yield return context.ConnectAll(); | ||
| Assert.IsNull(context.ConnectionError, context.ConnectionError); | ||
|
|
||
| var publisherRoom = context.Rooms[0]; | ||
| var subscriberRoom = context.Rooms[1]; | ||
|
|
||
| const uint expectedFrameId = 42u; | ||
| const ulong expectedUserTs = 0x1122334455667788UL; | ||
|
|
||
| var source = new TestVideoSource(pushFrames: true); | ||
| source.MetadataProvider = () => new FrameMetadata | ||
| { | ||
| FrameId = expectedFrameId, | ||
| UserTimestamp = expectedUserTs, | ||
| }; | ||
|
|
||
| var localTrack = LocalVideoTrack.CreateVideoTrack(VideoTrackName, source, publisherRoom); | ||
|
|
||
| var subscribedExp = new Expectation(timeoutSeconds: 10f); | ||
| RemoteVideoTrack receivedRemoteTrack = null; | ||
| subscriberRoom.TrackSubscribed += (track, _, _) => | ||
| { | ||
| if (track is RemoteVideoTrack rv) | ||
| { | ||
| receivedRemoteTrack = rv; | ||
| subscribedExp.Fulfill(); | ||
| } | ||
| }; | ||
|
|
||
| var options = new TrackPublishOptions | ||
| { | ||
| Source = TrackSource.SourceCamera, | ||
| }.WithPacketTrailerFeatures( | ||
| PacketTrailerFeature.PtfUserTimestamp, | ||
| PacketTrailerFeature.PtfFrameId); | ||
| var pub = publisherRoom.LocalParticipant.PublishTrack(localTrack, options); | ||
| yield return pub; | ||
| Assert.IsFalse(pub.IsError); | ||
|
|
||
| // Host the source's Update coroutine on a throwaway MonoBehaviour so the | ||
| // test body can yield on Expectations without owning the producer loop. | ||
| var runnerObj = new GameObject("metadata-test-runner"); | ||
| var runner = runnerObj.AddComponent<CoroutineRunner>(); | ||
| source.Start(); | ||
| runner.StartCoroutine(source.Update()); | ||
|
|
||
| yield return subscribedExp.Wait(); | ||
| Assert.IsNull(subscribedExp.Error); | ||
| Assert.IsNotNull(receivedRemoteTrack); | ||
|
|
||
| var stream = new VideoStream(receivedRemoteTrack); | ||
| var metadataExp = new Expectation(timeoutSeconds: 10f); | ||
| FrameMetadata receivedMetadata = null; | ||
| stream.FrameReceived += frame => | ||
| { | ||
| if (receivedMetadata != null || frame.Metadata == null) return; | ||
| receivedMetadata = frame.Metadata; | ||
| metadataExp.Fulfill(); | ||
| }; | ||
| stream.Start(); | ||
| runner.StartCoroutine(stream.Update()); | ||
|
|
||
| yield return metadataExp.Wait(); | ||
| Assert.IsNull(metadataExp.Error, "expected a frame with metadata within timeout"); | ||
| Assert.IsNotNull(receivedMetadata); | ||
| Assert.AreEqual(expectedFrameId, receivedMetadata.FrameId, "frame_id mismatch"); | ||
| Assert.AreEqual(expectedUserTs, receivedMetadata.UserTimestamp, "user_timestamp mismatch"); | ||
|
|
||
| source.Stop(); | ||
| stream.Stop(); | ||
| stream.Dispose(); | ||
| source.Dispose(); | ||
| Object.Destroy(runnerObj); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.