Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c641188
chore(mediaplayer): align embedded package with the BasisVR/BasisMedi…
towneh Jul 9, 2026
a38ab9d
fix(mediaplayer): interleave fMP4 audio and video by decode time
towneh Jul 7, 2026
f2abb2d
fix(mediaplayer): size the Annex B buffer for worst-case NAL growth
towneh Jul 7, 2026
0fea6cb
feat(mediaplayer): expose media metadata and add a playlist component
towneh Jul 7, 2026
ecbbdb1
fix(mediaplayer): load the first playlist entry on Start
towneh Jul 7, 2026
8fa879c
fix: harden metadata origin persistence, playlist advance, and null-U…
towneh Jul 7, 2026
2f9f1b8
feat: custom inspector for the playlist component
towneh Jul 7, 2026
c4a9275
fix: rebuild metadata for a new source instance with a matching origin
towneh Jul 7, 2026
c5cd422
feat: show current media title in the player inspector
towneh Jul 7, 2026
5f54295
feat: labelled rows on the Now Playing inspector card
towneh Jul 7, 2026
a361380
feat(mediaplayer): demux progressive (non-fragmented) MP4
towneh Jul 7, 2026
e26c0c8
fix(mediaplayer): guard classic sample tables against duplicate boxes
towneh Jul 7, 2026
133a740
fix(mediaplayer): harden MP4/RTMP demux against malformed input
towneh Jul 8, 2026
bb5b6c8
style(mediaplayer): tidy MP4 demuxer leftovers
towneh Jul 8, 2026
73c3044
fix(mediaplayer): apply track edit lists to progressive MP4 timestamps
towneh Jul 8, 2026
e5d5442
fix(mediaplayer): pace delivery on decode time, not presentation time
towneh Jul 8, 2026
fbea794
chore(mediaplayer): rebuild native binaries
towneh Jul 8, 2026
d6468fe
fix(mediaplayer): scope load identity to every source replacement
towneh Jul 8, 2026
7a670fb
feat(mediaplayer): report VOD duration from the native engine
towneh Jul 8, 2026
e05d80e
feat(mediaplayer): absolute seek for progressive MP4 and TS-segment H…
towneh Jul 8, 2026
9008893
feat(mediaplayer): route Duration, Seek and StartPosition through the…
towneh Jul 8, 2026
aa137ac
feat(mediaplayer): re-enable position sync now the backend can seek
towneh Jul 8, 2026
93b0d1f
feat(mediaplayer): seek bar and now-playing details on the Media Play…
towneh Jul 8, 2026
89927f9
fix(mediaplayer): report the presented position, not the decode position
towneh Jul 8, 2026
9f48581
chore(mediaplayer): rebuild native binaries
towneh Jul 8, 2026
ed10059
fix(mediaplayer): keep player-supplied text inert in the panel's TMP …
towneh Jul 9, 2026
7294d88
feat(integration.ytdlp): carry resolved metadata on the media source
towneh Jul 7, 2026
d7c63b8
feat(mediaplayer): enable HLS playback on Android
towneh Jul 7, 2026
fa9452b
feat(mediaplayer): LPCM decoder bypass on Android
towneh Jul 9, 2026
5abcb3c
fix(mediaplayer): steer m2ts past the Android OS extractor
towneh Jul 9, 2026
695db4e
chore(mediaplayer): rebuild libbasis_media_native.so (arm64-v8a)
towneh Jul 9, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ public static async Task<BasisMediaSource> ResolveSourceAsync(string pageUrl, Ca
BasisMediaSource source = SelectSource(info);
if (source == null || string.IsNullOrEmpty(source.Uri))
throw new YtDlpException($"yt-dlp returned no player-ingestible format for '{BasisMediaUrlRouter.Redact(pageUrl)}'.");
// Carry display metadata on the source: the player keys its metadata on
// the page URL (matching what networking syncs, not the per-client CDN
// endpoint) and shows the real title. Everything here comes from the
// extraction that just ran — no extra fetch.
source.Metadata = new BasisMediaMetadata
{
SourceUrl = pageUrl,
Title = info.Title,
Uploader = info.Uploader,
ThumbnailUrl = info.Thumbnail,
Duration = info.Duration.HasValue && info.Duration.Value > 0
? TimeSpan.FromSeconds(info.Duration.Value)
: (TimeSpan?)null,
Provider = "ytdlp",
};
return source;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

[CustomEditor(typeof(BasisMediaPlayerPlaylist))]
public class BasisMediaPlayerPlaylistInspector : Editor
{
private const string UxmlPath = "Packages/com.basis.mediaplayer/Editor/StyleSheets/MediaPlayerPlaylistSDK.uxml";
private const string UssPath = "Packages/com.basis.mediaplayer/Editor/StyleSheets/MediaPlayerSDK.uss";

private BasisMediaPlayerPlaylist _target;
private VisualElement _root;
private Label _status;
private Button _previousBtn, _nextBtn;

public override VisualElement CreateInspectorGUI()
{
_target = (BasisMediaPlayerPlaylist)target;
_root = new VisualElement();

var tree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
var sheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
if (tree == null)
{
_root.Add(new HelpBox("MediaPlayerPlaylistSDK.uxml missing.", HelpBoxMessageType.Error));
return _root;
}
tree.CloneTree(_root);
if (sheet != null) _root.styleSheets.Add(sheet);
_root.Bind(serializedObject);

_status = _root.Q<Label>("StatusLabel");
_previousBtn = _root.Q<Button>("PreviousButton");
_nextBtn = _root.Q<Button>("NextButton");

if (_previousBtn != null) _previousBtn.clicked += () => { if (_target != null) _target.Previous(); };
if (_nextBtn != null) _nextBtn.clicked += () => { if (_target != null) _target.Next(); };

_root.schedule.Execute(Refresh).Every(250);
Refresh();
return _root;
}

private void Refresh()
{
bool playing = Application.isPlaying;
DisplayStyle buttons = playing ? DisplayStyle.Flex : DisplayStyle.None;
if (_previousBtn != null) _previousBtn.style.display = buttons;
if (_nextBtn != null) _nextBtn.style.display = buttons;

if (_status == null || _target == null) return;
int count = _target.Entries != null ? _target.Entries.Count : 0;
if (!playing)
{
_status.text = count == 1 ? "1 entry" : $"{count} entries";
return;
}
int i = _target.CurrentIndex;
if (i < 0 || i >= count)
{
_status.text = $"Nothing loaded from this playlist yet ({count} entries).";
return;
}
var entry = _target.Entries[i];
string name = entry == null ? "" : (!string.IsNullOrEmpty(entry.DisplayName) ? entry.DisplayName : entry.Url);
_status.text = $"Entry {i + 1}/{count} — {name}";
}
}

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,27 @@
<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<engine:VisualElement class="bvp-root">
<engine:VisualElement name="HeaderCard" class="bvp-card bvp-header-card">
<engine:Label text="Basis Media Playlist" name="HeaderTitle" selectable="false" class="bvp-header-title"/>
<engine:Label text="Ordered play queue for a Basis Media Player" name="HeaderSubtitle" selectable="false" class="bvp-header-subtitle"/>
</engine:VisualElement>

<engine:Foldout text="Playlist" name="PlaylistFoldout" value="true" class="bvp-foldout">
<editor:PropertyField name="PlayerField" binding-path="Player"/>
<editor:PropertyField name="EntriesField" binding-path="Entries"/>
</engine:Foldout>

<engine:Foldout text="Behaviour" name="BehaviourFoldout" value="true" class="bvp-foldout">
<editor:PropertyField name="AdvanceField" binding-path="Advance"/>
<editor:PropertyField name="PlayOnStartField" binding-path="PlayOnStart"/>
</engine:Foldout>

<engine:VisualElement name="StatusCard" class="bvp-card">
<engine:Label text="" name="StatusLabel" selectable="false" class="bvp-mini-note"/>
</engine:VisualElement>

<engine:VisualElement name="ActionsCard" class="bvp-card bvp-actions-card">
<engine:Button text="Previous" name="PreviousButton" class="bvp-secondary-button"/>
<engine:Button text="Next" name="NextButton" class="bvp-primary-button"/>
</engine:VisualElement>
</engine:VisualElement>
</engine:UXML>

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
Expand Up @@ -84,6 +84,12 @@ struct basis_decoder {
int asr, ach;
int apcm_float; /* decoder emits float PCM (pcm-encoding 4) instead of 16-bit */

basis_codec_t ac; /* audio lane: AAC (MediaCodec) or LPCM (direct convert) */
int aLpcmAssign; /* Blu-ray channel_assignment (from the format's config blob) */
int aLpcmBits; /* 16 or 24 */
float* lpcmBuf; /* conversion scratch, grown to the largest frame batch */
int lpcmBufCap;

basis_vk_present* vk;

pthread_t worker;
Expand Down Expand Up @@ -276,6 +282,7 @@ void basis_decoder_destroy(basis_decoder_t* d) {
if (d->reader) AImageReader_delete(d->reader);
if (d->vk) basis_vk_destroy(d->vk);
ring_free(&d->pcm);
free(d->lpcmBuf);
free(d);
}

Expand Down Expand Up @@ -351,7 +358,28 @@ int basis_decoder_set_video_format(basis_decoder_t* d, basis_codec_t codec,

int basis_decoder_set_audio_format(basis_decoder_t* d, basis_codec_t codec,
int sample_rate, int channels, const uint8_t* asc, int asc_len) {
if (!d || d->aconfigured || codec != BASIS_CODEC_AAC) return 0;
if (!d || d->aconfigured) return 0;

if (codec == BASIS_CODEC_LPCM) {
/* Decoder bypass, mirroring the Windows lane: no MediaCodec involved —
* submit_audio converts straight into the ring. 48 kHz / 16- or 24-bit
* only; the TS demuxer filters to these before announcing, this is the
* matching backstop. The config blob carries the Blu-ray
* channel_assignment + bits code. */
if (sample_rate != 48000 || channels < 1 || channels > 8 || !asc || asc_len < 2) return 0;
int bits = asc[1] == 1 ? 16 : asc[1] == 3 ? 24 : 0;
if (!bits) return 0; /* 20-bit unsupported */
d->ac = BASIS_CODEC_LPCM;
d->asr = sample_rate; d->ach = channels;
d->aLpcmAssign = asc[0];
d->aLpcmBits = bits;
ring_set_frame(&d->pcm, channels);
d->aconfigured = 1;
return 0;
}

if (codec != BASIS_CODEC_AAC) return 0;
d->ac = BASIS_CODEC_AAC;
d->asr = sample_rate; d->ach = channels;
ring_set_frame(&d->pcm, channels ? channels : 2);
AMediaFormat* fmt = AMediaFormat_new();
Expand Down Expand Up @@ -389,8 +417,61 @@ int basis_decoder_submit_video(basis_decoder_t* d, const uint8_t* annexb, int le
return 0;
}

/* Source-order -> WAVE-order channel map for the Blu-ray HDMV LPCM
* channel_assignment values whose stream order differs from WAVE (Blu-ray
* places the LFE last and the side pair before the rears). Same tables as the
* Windows lane, which match ffmpeg's pcm_bluray remap for assignments 9 (5.1),
* 10 (7.0) and 11 (7.1) and were verified by ear against a 7.1 channel-marker
* stream. NULL = identity (mono/stereo/3.0/4.0/5.0 already arrive in WAVE
* order). */
static const int* lpcm_remap(int assign) {
static const int k51[6] = { 0, 1, 2, 4, 5, 3 };
static const int k70[7] = { 0, 1, 2, 5, 3, 4, 6 };
static const int k71[8] = { 0, 1, 2, 6, 4, 5, 7, 3 };
if (assign == 9) return k51;
if (assign == 10) return k70;
if (assign == 11) return k71;
return NULL;
}

/* LPCM bypass: big-endian 16/24-bit Blu-ray-order PCM -> interleaved WAVE-order
* float, straight into the ring. Whole frames only, so the ring keeps its
* channel phase (the alignment contract in the ring comment above). */
static void submit_lpcm(basis_decoder_t* d, const uint8_t* p, int len) {
int ch = d->ach;
int bytes = d->aLpcmBits / 8;
int frame_bytes = ch * bytes;
int frames = frame_bytes > 0 ? len / frame_bytes : 0;
if (frames <= 0) return;
int floats = frames * ch;
if (floats > d->lpcmBufCap) {
float* nb = (float*)realloc(d->lpcmBuf, sizeof(float) * (size_t)floats);
if (!nb) return;
d->lpcmBuf = nb; d->lpcmBufCap = floats;
}
const int* map = lpcm_remap(d->aLpcmAssign);
for (int f = 0; f < frames; ++f) {
const uint8_t* s = p + f * frame_bytes;
float* o = d->lpcmBuf + f * ch;
for (int c = 0; c < ch; ++c) {
int oc = map ? map[c] : c;
if (bytes == 2) {
int v = (int16_t)((s[c * 2] << 8) | s[c * 2 + 1]);
o[oc] = v / 32768.0f;
} else {
int v = (s[c * 3] << 16) | (s[c * 3 + 1] << 8) | s[c * 3 + 2];
if (v & 0x800000) v -= 0x1000000;
o[oc] = v / 8388608.0f;
}
}
}
ring_write(&d->pcm, d->lpcmBuf, floats);
}

int basis_decoder_submit_audio(basis_decoder_t* d, const uint8_t* data, int len, int64_t pts_us) {
if (!d || !d->acodec) return -1;
if (!d || !data || len <= 0) return -1;
if (d->ac == BASIS_CODEC_LPCM) { submit_lpcm(d, data, len); return 0; }
if (!d->acodec) return -1;
ssize_t ii = AMediaCodec_dequeueInputBuffer(d->acodec, 2000);
if (ii >= 0) {
size_t cap = 0;
Expand Down
Loading
Loading