ソースを参照

arozcast optimization

Toby Chui 1 週間 前
コミット
5eb1a9f7ca
4 ファイル変更131 行追加68 行削除
  1. 53 20
      src/web/Arozcast/index.html
  2. 60 31
      src/web/Movie/index.html
  3. 17 16
      src/web/Musicify/musicify.js
  4. 1 1
      src/web/Photo/photo.js

+ 53 - 20
src/web/Arozcast/index.html

@@ -181,8 +181,31 @@
             flex: 1; text-align: center;
             font-size: 13px; font-weight: 500;
             color: var(--text);
-            overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+            overflow: hidden;
             padding: 0 4px;
+            display: flex; flex-direction: column; align-items: center; gap: 1px;
+        }
+        .tb-filename-main {
+            overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+            max-width: 100%;
+        }
+        .tb-roomcode {
+            font-size: 10px; opacity: 0.5; letter-spacing: 0.07em;
+            white-space: nowrap;
+        }
+
+        /* ── Disconnected banner (sender dropped; media still playing) ── */
+        .disconnected-banner {
+            position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
+            z-index: 300;
+            background: rgba(20,20,30,0.82);
+            backdrop-filter: blur(10px);
+            border: 1px solid rgba(255,255,255,.1);
+            border-radius: 20px;
+            padding: 6px 18px;
+            font-size: 11px; color: rgba(255,255,255,0.75);
+            white-space: nowrap;
+            pointer-events: none;
         }
 
         /* ── Disconnected notice ────────────────────────────────────── */
@@ -313,6 +336,12 @@
            @ended="onMediaEnded()">
     </audio>
 
+    <!-- ── Disconnected banner (sender dropped but media still playing) ── -->
+    <div class="disconnected-banner"
+         x-show="currentTrack && senderState === 'disconnected'"
+         x-text="'Sender disconnected · room ' + roomCode + ' · playback continues'">
+    </div>
+
     <!-- ── Bottom toolbar ────────────────────────────────────────── -->
     <div class="toolbar" :class="{hidden: toolbarHidden && currentTrack && mediaType !== 'photo'}"
          @mousemove="showToolbar()" @mouseenter="showToolbar()">
@@ -322,8 +351,11 @@
             <img :src="isFullscreen ? 'img/fullscreen_exit.svg' : 'img/fullscreen.svg'" style="width:20px;height:20px;">
         </button>
 
-        <!-- Filename -->
-        <span class="tb-filename" x-text="currentTrack ? currentTrack.name : 'Arozcast'"></span>
+        <!-- Filename + room code -->
+        <span class="tb-filename">
+            <span class="tb-filename-main" x-text="currentTrack ? currentTrack.name : 'Arozcast'"></span>
+            <span class="tb-roomcode" x-show="roomCode" x-text="'Room ' + roomCode"></span>
+        </span>
 
         <!-- Mute toggle -->
         <button class="tb-btn" @click="toggleMute()" :title="isMuted ? 'Unmute' : 'Mute'">
@@ -515,8 +547,8 @@ function arozcastApp() {
             this._peerTimer = setTimeout(() => {
                 self.peerCount = 0;
                 self.senderState = 'disconnected';
-                self._stop();
-                self._showToast('Sender disconnected');
+                // Intentionally do NOT stop playback — sender may be reconnecting
+                self._showToast('Sender disconnected — playback continues');
             }, 12000);
         },
 
@@ -530,35 +562,36 @@ function arozcastApp() {
             const type = track.type || 'audio';
             this.mediaType = type;
 
-            const fileUrl = ao_root + 'media?file=' + encodeURIComponent(track.filepath);
+            // Prefer the URL provided by the sender (respects transcoding API etc.);
+            // fall back to direct media API if not supplied.
+            const fileUrl = track.src || (ao_root + 'media?file=' + encodeURIComponent(track.filepath));
 
             const startTime = parseFloat(track.startTime) || 0;
+            const self = this;
 
             if (type === 'audio') {
                 this.coverSrc = ao_root + 'system/file_system/loadThumbnail?bytes=true&vpath=' + encodeURIComponent(track.filepath);
                 this._audio.src = fileUrl;
-                this._audio.volume = this.volume / 100;
-                this._audio.muted = this.isMuted;
                 this._audio.load();
-                if (startTime > 0) {
-                    this._audio.addEventListener('loadedmetadata', () => {
-                        this._audio.currentTime = startTime;
-                    }, { once: true });
-                }
+                // Apply volume and seek inside loadedmetadata so browser-assigned
+                // defaults cannot overwrite our values after load() returns.
+                this._audio.addEventListener('loadedmetadata', () => {
+                    self._audio.volume = self.volume / 100;
+                    self._audio.muted  = self.isMuted;
+                    if (startTime > 0) self._audio.currentTime = startTime;
+                }, { once: true });
                 this._audio.play().catch(() => {});
                 this.isPlaying = true;
                 this._video.pause();
                 this._video.removeAttribute('src');
             } else if (type === 'video') {
                 this._video.src = fileUrl;
-                this._video.volume = this.volume / 100;
-                this._video.muted = this.isMuted;
                 this._video.load();
-                if (startTime > 0) {
-                    this._video.addEventListener('loadedmetadata', () => {
-                        this._video.currentTime = startTime;
-                    }, { once: true });
-                }
+                this._video.addEventListener('loadedmetadata', () => {
+                    self._video.volume = self.volume / 100;
+                    self._video.muted  = self.isMuted;
+                    if (startTime > 0) self._video.currentTime = startTime;
+                }, { once: true });
                 this._video.play().catch(() => {});
                 this.isPlaying = true;
                 this._audio.pause();

+ 60 - 31
src/web/Movie/index.html

@@ -1468,7 +1468,7 @@ function _castResumeLocally() {
 }
 
 // ── Arozcast auto-reconnect ───────────────────────────────────────────────────
-var _CAST_RECONNECT_DELAYS = [2000, 4000, 8000, 16000, 30000];
+var _CAST_RECONNECT_DELAYS = [2000, 5000, 12000];
 
 function _startCastReconnect(code) {
     if (!code || castReconnectCount >= _CAST_RECONNECT_DELAYS.length) {
@@ -1517,25 +1517,18 @@ function _castDidReconnect(ws, code) {
         $('#ctrl-cast, #ctrl-cast-top').removeClass('casting');
         if (wasActive) { _startCastReconnect(savedCode); }
     };
-    // Re-announce and restore full media state at the last known remote position
+    // Re-announce presence and sync volume only — do NOT resend media.load.
+    // Arozcast kept playing while the phone was asleep; its next status.update
+    // will immediately sync castCurrentTime to the live remote position.
     _castSend('peer.hello', {});
     var vid = document.getElementById('main-video');
     _castSend('media.volume', { volume: vid.volume * 100, muted: vid.muted });
-    if (playingIndex >= 0 && currentEpisodes && currentEpisodes[playingIndex]) {
-        var ep  = currentEpisodes[playingIndex];
-        var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
-        var src = isWebPlayable(ext)
-            ? MEDIA_API + '?file=' + encodeURIComponent(ep.filepath)
-            : TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath);
-        _castSend('media.load', { filepath: ep.filepath, name: ep.name, type: 'video', src: src, startTime: castCurrentTime });
-        _castSend(castIsPlaying ? 'media.play' : 'media.pause', {});
-    }
     castPingTimer  = setInterval(function() { _castSend('peer.heartbeat', {}); }, 5000);
     castWatchTimer = setInterval(function() {
         if (Date.now() - castLastSeen > 12000 && castWs) { castWs.close(); }
     }, 4000);
     $('#ctrl-cast, #ctrl-cast-top').addClass('casting');
-    showToast('Arozcast reconnected — resuming from ' + formatTime(castCurrentTime));
+    showToast('Arozcast reconnected');
 }
 
 function _handleCastMessage(msg) {
@@ -2012,13 +2005,10 @@ $(document).ready(function () {
         }
     });
 
-    // Clean up cast on page close
+    // Close WS cleanly on page unload — do NOT send media.stop so Arozcast keeps playing.
+    // Only an explicit disconnectCast() call should stop remote playback.
     window.addEventListener('beforeunload', function() {
-        if (_castConnected()) {
-            _castSend('media.stop', {});
-            castWs.onclose = null;
-            castWs.close();
-        }
+        if (castWs) { castWs.onclose = null; castWs.close(); }
     });
 });
 
@@ -2518,11 +2508,10 @@ function closePlayer() {
     // Always cancel any pending auto-reconnect
     clearTimeout(castReconnectTimer); castReconnectTimer = null;
     castReconnectCount = 0; castPendingCode = null;
-    if (castMode && _castConnected()) {
-        _castSend('media.stop', {});
-        castWs.onclose = null;
-        castWs.close();
-        castWs = null;
+    if (castMode) {
+        // Do NOT send media.stop — Arozcast will keep playing.
+        // Only disconnectCast() (explicit user action) stops remote playback.
+        if (castWs) { castWs.onclose = null; castWs.close(); castWs = null; }
         clearInterval(castPingTimer); clearInterval(castWatchTimer);
         castPingTimer = castWatchTimer = null;
         castMode = false; castCode = null;
@@ -2612,7 +2601,11 @@ function initVideoControls() {
         if (castMode && _castConnected()) {
             if (castDuration > 0) {
                 var pct = e.offsetX / $(this).width();
-                _castSend('media.seek', { time: pct * castDuration });
+                var seekTo = pct * castDuration;
+                _castSend('media.seek', { time: seekTo });
+                // Optimistic update — reflect position immediately
+                castCurrentTime = seekTo;
+                _castUpdateProgressUI();
             }
             return;
         }
@@ -2682,10 +2675,28 @@ function initVideoControls() {
     }
 }
 
+// Optimistic UI sync for cast mode — call immediately after sending a seek/play/pause command
+// so the user sees instant feedback instead of waiting up to 3 s for status.update.
+function _castUpdateProgressUI() {
+    if (!castDuration) return;
+    var pct = (castCurrentTime / castDuration) * 100;
+    $('#progress-bar').css('width', pct + '%');
+    $('#progress-thumb').css('left', 'calc(' + pct + '% - 7px)');
+    $('#time-display').text(formatTime(castCurrentTime) + ' / ' + formatTime(castDuration));
+}
+
 function togglePlay() {
     $('#resume-popup').removeClass('active');
     if (castMode && _castConnected()) {
-        _castSend(castIsPlaying ? 'media.pause' : 'media.play', {});
+        if (castIsPlaying) {
+            _castSend('media.pause', {});
+            castIsPlaying = false;
+            $('#play-icon').attr('src', 'img/icons/play_white.svg');
+        } else {
+            _castSend('media.play', {});
+            castIsPlaying = true;
+            $('#play-icon').attr('src', 'img/icons/pause_white.svg');
+        }
         return;
     }
     var vid = document.getElementById('main-video');
@@ -2784,8 +2795,20 @@ function initContextMenu() {
         if (e.key === 'Escape') { $ctx.hide(); closeVideoInfo(); }
     });
 
-    $('#ctx-play').on('click',   function () { if (castMode && _castConnected()) { _castSend('media.play',{}); } else { vid.play(); } $ctx.hide(); });
-    $('#ctx-pause').on('click',  function () { if (castMode && _castConnected()) { _castSend('media.pause',{}); } else { vid.pause(); } $ctx.hide(); });
+    $('#ctx-play').on('click', function () {
+        if (castMode && _castConnected()) {
+            _castSend('media.play', {}); castIsPlaying = true;
+            $('#play-icon').attr('src', 'img/icons/pause_white.svg');
+        } else { vid.play(); }
+        $ctx.hide();
+    });
+    $('#ctx-pause').on('click', function () {
+        if (castMode && _castConnected()) {
+            _castSend('media.pause', {}); castIsPlaying = false;
+            $('#play-icon').attr('src', 'img/icons/play_white.svg');
+        } else { vid.pause(); }
+        $ctx.hide();
+    });
     $('#ctx-prev').on('click',   function () { cancelCountdown(); playOffset(-1); $ctx.hide(); });
     $('#ctx-next').on('click',   function () { cancelCountdown(); playOffset(1);  $ctx.hide(); });
     $('#ctx-repeat').on('click', function () { repeatSingle = !repeatSingle;      $ctx.hide(); });
@@ -2893,13 +2916,19 @@ function initKeyboard() {
                     e.preventDefault(); togglePlay(); showControls(); break;
                 case 'ArrowRight':
                     e.preventDefault();
-                    if (castMode && _castConnected()) { _castSend('media.seekrel', { delta: 10 }); }
-                    else { vid.currentTime = Math.min(vid.duration || 0, vid.currentTime + 10); }
+                    if (castMode && _castConnected()) {
+                        _castSend('media.seekrel', { delta: 10 });
+                        castCurrentTime = Math.min(castDuration, castCurrentTime + 10);
+                        _castUpdateProgressUI();
+                    } else { vid.currentTime = Math.min(vid.duration || 0, vid.currentTime + 10); }
                     showControls(); break;
                 case 'ArrowLeft':
                     e.preventDefault();
-                    if (castMode && _castConnected()) { _castSend('media.seekrel', { delta: -10 }); }
-                    else { vid.currentTime = Math.max(0, vid.currentTime - 10); }
+                    if (castMode && _castConnected()) {
+                        _castSend('media.seekrel', { delta: -10 });
+                        castCurrentTime = Math.max(0, castCurrentTime - 10);
+                        _castUpdateProgressUI();
+                    } else { vid.currentTime = Math.max(0, vid.currentTime - 10); }
                     showControls(); break;
                 case 'ArrowUp':
                     e.preventDefault(); vid.volume = Math.min(1, vid.volume + 0.1);

+ 17 - 16
src/web/Musicify/musicify.js

@@ -1380,7 +1380,7 @@ function musicifyApp() {
         // ── Auto-reconnect helpers ────────────────────────────────────────────
         _startCastReconnect(code) {
             var self = this;
-            var DELAYS = [2000, 4000, 8000, 16000, 30000];
+            var DELAYS = [2000, 5000, 12000];
             if (!code || this._castReconnectCount >= DELAYS.length) {
                 if (this._castReconnectCount > 0) {
                     // All retries exhausted — fall back to local playback
@@ -1456,29 +1456,23 @@ function musicifyApp() {
                 self.castConnected = false; self.castMode = false; self._castWs = null;
                 if (wasActive) { self._startCastReconnect(savedCode); }
             };
-            // Re-announce and restore full media state at the last known remote position
+            // Re-announce presence and sync volume only — do NOT resend media.load.
+            // Arozcast kept playing while the phone was asleep; its next status.update
+            // will immediately sync currentTime to the live remote position.
             ws.send(JSON.stringify({ topic: 'peer.hello', payload: {} }));
             this._castSend('media.volume', { volume: this.volume, muted: this.isMuted });
-            if (this.currentTrack) {
-                this._castSend('media.load', {
-                    filepath: this.currentTrack.filepath,
-                    name: this.currentTrack.name,
-                    artist: this.getArtistLabel(this.currentTrack),
-                    cover: this.currentTrack.cover || '',
-                    type: 'audio',
-                    startTime: this.currentTime
-                });
-                this._castSend(this.isPlaying ? 'media.play' : 'media.pause', {});
-            }
             clearInterval(this._castPingTimer); clearInterval(this._castWatchTimer);
             this._castPingTimer = setInterval(function() { self._castSend('peer.heartbeat', {}); }, 5000);
             this._castWatchTimer = setInterval(function() {
                 if (Date.now() - self._castLastSeen > 12000 && self._castWs) self._castWs.close();
             }, 4000);
-            this._showToast('Arozcast reconnected — resuming');
+            this._showToast('Arozcast reconnected');
         },
 
         disconnectCast() {
+            // Capture play state before we tear anything down
+            var wasPlaying = this.isPlaying;
+
             // Cancel any pending auto-reconnect before tearing down
             clearTimeout(this._castReconnectTimer); this._castReconnectTimer = null;
             this._castReconnectCount = 0; this._castPendingCode = null;
@@ -1490,6 +1484,9 @@ function musicifyApp() {
             this.castConnected = false;
             this.showCastModal = false;
             if (this._castWs) {
+                // Send stop so Arozcast halts — this is an explicit user disconnect,
+                // not a sleep/drop (those suppress onclose and never reach here).
+                this._castSend('media.stop', {});
                 this._castWs.onclose = null;   // suppress reconnect trigger
                 this._castWs.close();
                 this._castWs = null;
@@ -1497,8 +1494,10 @@ function musicifyApp() {
             this.castCode = '';
             this.castCodeInput = '';
             this.castError = '';
+            this.isPlaying = false;
 
-            // Resume local playback from current track
+            // Resume local playback at the last known remote position,
+            // but only auto-start if the remote was actually playing.
             if (this.currentTrack) {
                 var resumeAt = this.currentTime;
                 var self = this;
@@ -1511,7 +1510,9 @@ function musicifyApp() {
                         self._audio.currentTime = resumeAt;
                     }, { once: true });
                 }
-                this._audio.play().catch(function() {});
+                if (wasPlaying) {
+                    this._audio.play().catch(function() {});
+                }
             }
             this._showToast('Disconnected from Arozcast');
         },

+ 1 - 1
src/web/Photo/photo.js

@@ -1009,7 +1009,7 @@ function _photoCastSendPhoto(filepath) {
 }
 
 // ── Arozcast photo auto-reconnect ─────────────────────────────────────────────
-var _PHOTO_CAST_RECONNECT_DELAYS = [2000, 4000, 8000, 16000, 30000];
+var _PHOTO_CAST_RECONNECT_DELAYS = [2000, 5000, 12000];
 
 function _startPhotoCastReconnect(code) {
     if (!code || _photoCastReconnectCount >= _PHOTO_CAST_RECONNECT_DELAYS.length) {