DeadAir made a shortened copy of each episode with ffmpeg: as much disk again as the original, one pause length for everyone, and a minutes-long wait after the download. Seeking past silences instead was tried on an iPhone and left about 1.3 s of nothing at every cut (#178). Now it finds a file's silences once (silencedetect, plus ffprobe for each frame's byte position) and stores a plan for each pause a person can choose, 0.2 to 0.5 s: the cuts, and the frames kept as byte ranges of the original. /media/{id}/deadair.m3u8 hands this person's plan out as an HLS playlist of ranges of /media/{id}/deadair.mp3, the original. Tested on iOS Safari first (#180): no gap at the cuts, plays locked, seeks exactly, and needs no ID3 timestamp in front of each piece. Other browsers get hls.js (1.6.19, the light build), loaded only when needed. Not where canPlayType says it can play HLS: Chromium answers "maybe" and then fails on MP3 ranges with "stream parsing failed", so only Apple's browsers play the playlist themselves. The pause is per person and per show, on deadair_prefs.pause_ms, 0.3 s unless chosen; 0.2 s, what the copies left, made speech sound rushed. AAC in MP4 has no ranges a playlist can use, so only MP3 and ADTS AAC are shortened (the library is 803 MP3s and no .m4a). The copies, their table and #179's expiry go: the first start drops the table and deletes the files it names, only those named name.deadair.*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,34 @@ const audio=$('#audio');
|
||||
const player: {guid: string|null, feed: string|null, entry: any, enc?: number, src?: string, moved?: boolean,
|
||||
saveAt: number, marked: boolean}={guid:null,feed:null,entry:null,saveAt:0,marked:false};
|
||||
|
||||
/// Where a file plays from: DeadAir's copy when the server says this person plays it (#171),
|
||||
/// whose times the entry is already given in, else the file itself.
|
||||
const mediaSrc=enc=>enc.deadair!=null?`/media/${enc.id}/deadair`:`/media/${enc.id}`;
|
||||
/// Where a file plays from: DeadAir's playlist when the server says this person plays it
|
||||
/// shortened (#171, #181), whose times the entry is already given in, else the file itself.
|
||||
const mediaSrc=enc=>enc.deadair!=null?`/media/${enc.id}/deadair.m3u8`:`/media/${enc.id}`;
|
||||
|
||||
/// hls.js, playing DeadAir's playlist where the browser cannot itself. Fetched the first time it
|
||||
/// is needed. Only Apple's browsers play it themselves: Chromium says it can ("maybe"), then fails
|
||||
/// on the playlist's MP3 ranges with "stream parsing failed", and so plays nothing.
|
||||
let hls: any=null;
|
||||
function dropHls(){ if(hls){ hls.destroy(); hls=null; } }
|
||||
/// Puts a file in the player: null when it is there at once, else a promise to wait on before
|
||||
/// playing, while hls.js loads. Safari always takes the first way, where play() has to run within
|
||||
/// the tap itself or it is refused.
|
||||
function setSrc(url: string): Promise<void>|null{
|
||||
dropHls();
|
||||
const native=navigator.vendor.startsWith('Apple')&&!!audio.canPlayType('application/vnd.apple.mpegurl');
|
||||
if(!url.endsWith('.m3u8')||native){ audio.src=url; return null; }
|
||||
const w=window as any;
|
||||
const ready=w.Hls?Promise.resolve():new Promise<void>((ok,fail)=>{
|
||||
const s=document.createElement('script');
|
||||
s.src='__HLS_JS__'; s.onload=()=>ok(); s.onerror=()=>fail(new Error('the DeadAir player did not load'));
|
||||
document.head.append(s);
|
||||
});
|
||||
return ready.then(()=>{
|
||||
hls=new w.Hls();
|
||||
hls.on(w.Hls.Events.ERROR,(_,d)=>{ if(d.fatal) toast('Playback failed: '+d.details,true); });
|
||||
hls.loadSource(url); hls.attachMedia(audio);
|
||||
});
|
||||
}
|
||||
|
||||
// Marked read when an item has actually been listened to -- at the end, or past 90%.
|
||||
// NOT on play: doing that made the item vanish from the Unread list the instant
|
||||
@@ -29,10 +54,11 @@ function play(e,enc=e.enclosures.find(isPlayable)){
|
||||
const resuming = player.guid===e.guid && player.enc===enc.id && player.src===mediaSrc(enc);
|
||||
// Every play button is a pause button for what is playing, as the player bar's is.
|
||||
if(resuming && !audio.paused){ audio.pause(); return; }
|
||||
let wait: Promise<void>|null=null;
|
||||
if(!resuming){
|
||||
player.guid=e.guid; player.feed=e.feed_id; player.entry=e; player.enc=enc.id; player.moved=false;
|
||||
document.body.classList.toggle('has-video', kindOf(enc)==='video');
|
||||
audio.src=player.src=mediaSrc(enc);
|
||||
wait=setSrc(player.src=mediaSrc(enc));
|
||||
audio.currentTime=0;
|
||||
if(e.position>5) audio.addEventListener('loadedmetadata',()=>{audio.currentTime=e.position},{once:true});
|
||||
// Initials, if it comes to that, are the feed's: the episode's read as "SE" beside the feed's art.
|
||||
@@ -45,7 +71,8 @@ function play(e,enc=e.enclosures.find(isPlayable)){
|
||||
player.marked=false;
|
||||
paintDead();
|
||||
}
|
||||
audio.play().catch(err=>toast('Playback failed: '+err.message,true));
|
||||
const go=()=>audio.play().catch(err=>toast('Playback failed: '+err.message,true));
|
||||
if(wait) wait.then(go,err=>toast('Playback failed: '+err.message,true)); else go();
|
||||
renderEntries();
|
||||
}
|
||||
function mediaSession(e,f){
|
||||
@@ -153,12 +180,12 @@ async function switchVersion(){
|
||||
if(!enc||mediaSrc(enc)===player.src) return false;
|
||||
const was=!audio.paused;
|
||||
player.entry=e; player.src=mediaSrc(enc); player.saveAt=e.position;
|
||||
audio.src=player.src;
|
||||
const wait=setSrc(player.src);
|
||||
audio.addEventListener('loadedmetadata',()=>{audio.currentTime=e.position},{once:true});
|
||||
if(was) audio.play().catch(()=>{});
|
||||
if(was) (wait||Promise.resolve()).then(()=>audio.play()).catch(()=>{});
|
||||
return true;
|
||||
}
|
||||
$('#pclose').onclick=()=>{savePos();audio.pause();audio.removeAttribute('src');player.guid=null;$('#player').classList.remove('on');document.body.classList.remove('has-video');renderEntries();
|
||||
$('#pclose').onclick=()=>{savePos();audio.pause();dropHls();audio.removeAttribute('src');player.guid=null;$('#player').classList.remove('on');document.body.classList.remove('has-video');renderEntries();
|
||||
// Called here, not left to the pause event: closing a player already paused fires none.
|
||||
syncListening()};
|
||||
(function restore(){
|
||||
|
||||
Reference in New Issue
Block a user