feat: native Android audio via Media3 ExoPlayer + full NSBridge JS adapter
- Replace legacy MediaBrowserServiceCompat/MediaSessionCompat with
Media3 MediaSessionService (MusicService.kt) + ExoPlayer set to
WAKE_MODE_NETWORK — holds WakeLock + WifiLock through Doze so audio
plays uninterrupted on locked screen
- Media3 manages the notification; no manual NotificationCompat.Builder
→ no disappear/reappear flicker when skipping tracks
- Full bidirectional bridge:
JS → native: window.NSBridge.* (WebBridge.kt, 13 methods)
Native → JS: window.NSPlayer.* callbacks (onReady, onTrackChanged,
onPositionUpdate, onPlaybackStateChanged, onQueueEnded, onError)
- player.js: progressive enhancement — all playback routes through
NSBridge when running inside the wrapper, falls back to S.audio on
plain PWA/browser; removes all window.Android.* calls
- main.js: guard _onForeground mediaSession playbackState setter behind
!window.NSBridge so native Media3 session is never stomped
- ProGuard rules updated to keep WebBridge @JavascriptInterface methods
and Media3 classes
- bridge-test.html: manual integration test page for every bridge method
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ede8727a2
commit
0e17d2e5db
15 files changed
+1093
-95
No files matched your search
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NSBridge Test</title>
|
||||
<style>
|
||||
body { font-family: monospace; background: #111; color: #eee; padding: 16px; }
|
||||
h2 { color: #8af; margin: 24px 0 8px; }
|
||||
button { background: #222; color: #8af; border: 1px solid #444; border-radius: 6px;
|
||||
padding: 8px 14px; margin: 4px; cursor: pointer; font-family: monospace; }
|
||||
button:hover { background: #333; }
|
||||
#log { background: #000; border: 1px solid #333; border-radius: 6px; padding: 12px;
|
||||
height: 220px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; }
|
||||
input { background: #222; color: #eee; border: 1px solid #444; border-radius: 4px;
|
||||
padding: 6px 10px; font-family: monospace; width: 280px; }
|
||||
.ok { color: #8f8; }
|
||||
.err { color: #f88; }
|
||||
.evt { color: #fa8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>NovaSonic Bridge Test</h1>
|
||||
<p>Load this page as a local asset to verify the NSBridge surface before connecting the real PWA.</p>
|
||||
|
||||
<div id="log">Waiting for bridge...\n</div>
|
||||
|
||||
<h2>Queue</h2>
|
||||
<button onclick="testSetQueue()">setQueue (2 tracks, autoPlay=true)</button>
|
||||
<button onclick="testAddToQueue()">addToQueue (append)</button>
|
||||
<button onclick="cmd('removeFromQueue', 1)">removeFromQueue(1)</button>
|
||||
<button onclick="cmd('clearQueue')">clearQueue</button>
|
||||
|
||||
<h2>Transport</h2>
|
||||
<button onclick="cmd('play')">play</button>
|
||||
<button onclick="cmd('pause')">pause</button>
|
||||
<button onclick="cmd('togglePlay')">togglePlay</button>
|
||||
<button onclick="cmd('next')">next</button>
|
||||
<button onclick="cmd('prev')">prev</button>
|
||||
<button onclick="testSeek()">seekTo(30000)</button>
|
||||
<button onclick="cmd('seekToIndex', 0, 0)">seekToIndex(0,0)</button>
|
||||
|
||||
<h2>Settings</h2>
|
||||
<button onclick="cmd('setRepeatMode','all')">repeat all</button>
|
||||
<button onclick="cmd('setRepeatMode','one')">repeat one</button>
|
||||
<button onclick="cmd('setRepeatMode','off')">repeat off</button>
|
||||
<button onclick="cmd('setShuffleMode',true)">shuffle on</button>
|
||||
<button onclick="cmd('setShuffleMode',false)">shuffle off</button>
|
||||
<button onclick="cmd('setVolume',0.5)">volume 50%</button>
|
||||
<button onclick="cmd('setPlaybackSpeed',1.5)">speed 1.5x</button>
|
||||
<button onclick="cmd('setPlaybackSpeed',1.0)">speed 1.0x</button>
|
||||
|
||||
<h2>State</h2>
|
||||
<button onclick="showState()">getState()</button>
|
||||
|
||||
<h2>NSPlayer callbacks (incoming from native)</h2>
|
||||
<p style="color:#aaa;font-size:12px">These fire automatically when native state changes.</p>
|
||||
|
||||
<script>
|
||||
const log = document.getElementById('log');
|
||||
|
||||
function print(msg, cls) {
|
||||
const line = document.createElement('span');
|
||||
line.className = cls || '';
|
||||
line.textContent = new Date().toISOString().slice(11,23) + ' ' + msg + '\n';
|
||||
log.appendChild(line);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
// ── Bridge availability check ─────────────────────────────────────────────
|
||||
|
||||
function hasBridge() {
|
||||
if (!window.NSBridge) { print('❌ window.NSBridge not found — not running inside NovaSonic app', 'err'); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Generic command helper ────────────────────────────────────────────────
|
||||
|
||||
function cmd(method, ...args) {
|
||||
if (!hasBridge()) return;
|
||||
try {
|
||||
const result = window.NSBridge[method](...args);
|
||||
print(`→ NSBridge.${method}(${args.map(a=>JSON.stringify(a)).join(', ')})${result !== undefined ? ' = ' + result : ''}`, 'ok');
|
||||
} catch(e) {
|
||||
print(`❌ NSBridge.${method}: ${e}`, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Queue helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
const DEMO_TRACKS = [
|
||||
{
|
||||
id: "track-1",
|
||||
url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
|
||||
title: "SoundHelix Song 1",
|
||||
artist: "T. Schürger",
|
||||
album: "Demo Album",
|
||||
artworkUrl: null,
|
||||
durationMs: 372000
|
||||
},
|
||||
{
|
||||
id: "track-2",
|
||||
url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3",
|
||||
title: "SoundHelix Song 2",
|
||||
artist: "T. Schürger",
|
||||
album: "Demo Album",
|
||||
artworkUrl: null,
|
||||
durationMs: 390000
|
||||
}
|
||||
];
|
||||
|
||||
function testSetQueue() {
|
||||
if (!hasBridge()) return;
|
||||
try {
|
||||
window.NSBridge.setQueue(JSON.stringify(DEMO_TRACKS), 0, true);
|
||||
print('→ NSBridge.setQueue([2 tracks], startIndex=0, autoPlay=true)', 'ok');
|
||||
} catch(e) { print('❌ setQueue: ' + e, 'err'); }
|
||||
}
|
||||
|
||||
function testAddToQueue() {
|
||||
if (!hasBridge()) return;
|
||||
const track = { id: "track-3", url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3",
|
||||
title: "SoundHelix Song 3", artist: "T. Schürger", album: null, artworkUrl: null, durationMs: 0 };
|
||||
try {
|
||||
window.NSBridge.addToQueue(JSON.stringify(track), -1);
|
||||
print('→ NSBridge.addToQueue(track-3, position=-1 [append])', 'ok');
|
||||
} catch(e) { print('❌ addToQueue: ' + e, 'err'); }
|
||||
}
|
||||
|
||||
function testSeek() { cmd('seekTo', 30000); }
|
||||
|
||||
function showState() {
|
||||
if (!hasBridge()) return;
|
||||
try {
|
||||
const raw = window.NSBridge.getState();
|
||||
print('← getState() = ' + raw, 'ok');
|
||||
} catch(e) { print('❌ getState: ' + e, 'err'); }
|
||||
}
|
||||
|
||||
// ── NSPlayer callbacks (Native → JS) ─────────────────────────────────────
|
||||
|
||||
window.NSPlayer = {
|
||||
onReady() {
|
||||
print('← NSPlayer.onReady()', 'evt');
|
||||
},
|
||||
onPlaybackStateChanged(stateJson) {
|
||||
try {
|
||||
const s = JSON.parse(stateJson);
|
||||
print(`← NSPlayer.onPlaybackStateChanged isPlaying=${s.isPlaying} isBuffering=${s.isBuffering}`, 'evt');
|
||||
} catch(e) { print('← NSPlayer.onPlaybackStateChanged (parse error): ' + stateJson, 'err'); }
|
||||
},
|
||||
onTrackChanged(payloadJson) {
|
||||
try {
|
||||
const p = JSON.parse(payloadJson);
|
||||
print(`← NSPlayer.onTrackChanged index=${p.index} trackId=${p.trackId} reason=${p.reason}`, 'evt');
|
||||
} catch(e) { print('← NSPlayer.onTrackChanged (parse error): ' + payloadJson, 'err'); }
|
||||
},
|
||||
onPositionUpdate(positionMs, durationMs) {
|
||||
// High-frequency — only log every 5 s to keep the log readable
|
||||
if (!window._lastPosPrint || Date.now() - window._lastPosPrint > 5000) {
|
||||
window._lastPosPrint = Date.now();
|
||||
print(`← NSPlayer.onPositionUpdate pos=${positionMs}ms dur=${durationMs}ms`, 'evt');
|
||||
}
|
||||
},
|
||||
onQueueEnded() {
|
||||
print('← NSPlayer.onQueueEnded()', 'evt');
|
||||
},
|
||||
onError(message) {
|
||||
print('← NSPlayer.onError: ' + message, 'err');
|
||||
},
|
||||
onMediaButtonAction(action) {
|
||||
print('← NSPlayer.onMediaButtonAction: ' + action, 'evt');
|
||||
}
|
||||
};
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (window.NSBridge) {
|
||||
print('✓ window.NSBridge detected', 'ok');
|
||||
} else {
|
||||
print('ℹ window.NSBridge not found — run inside NovaSonic app for full test', 'err');
|
||||
print(' (Callback handlers are registered; native can still fire them)', '');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user