Repository navigation
fix(playback): play local files and downloads on iOS via resource path (#100) - #184
Conversation
#100) Local files and downloaded episodes were played from in-memory blob: URLs, which iOS WKWebView cannot play: its media element loads audio out-of-process and cannot read an in-memory blob, so playback silently failed on iOS while streaming (a real https URL) worked. Desktop (Electron/Chromium) plays blob audio fine, which is why the bug was iOS-only. Use app.vault.getResourcePath() — the same native-served, byte-range-capable URL Obsidian uses for its own media embeds — for both local and downloaded playback. This also stops reading the entire audio file into memory as an ArrayBuffer just to play it. - Replace blob URL with getResourcePath in createMediaUrlObjectFromFilePath; remove the now-dead BlobUrlManager and its unload wiring in main.ts - Fix the context-menu extension check that matched every file (the inline regex had a trailing empty alternative) - Add unit tests for the resource-path helper - Note cross-platform playback in the local files docs Closes #100
Deploying podnotes with
|
| Latest commit: |
0ffda18
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://77f2011c.podnotes.pages.dev |
| Branch Preview URL: | https://chhoumann-100-local-files-io.podnotes.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 941a7983a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const PLAYABLE_EXTENSIONS = new Set([ | ||
| "mp3", | ||
| "mp4", | ||
| "wma", | ||
| "aac", | ||
| "wav", | ||
| "webm", | ||
| "flac", | ||
| "m4a", | ||
| ]); |
There was a problem hiding this comment.
Include all supported local audio extensions
For local files with extensions that the rest of PodNotes already treats as audio, such as .ogg and .amr (detectAudioFileExtension and getExtensionFromContentType both return those extensions), this new allow-list suppresses the Play with PodNotes menu item entirely. The old regex was overly broad, but replacing it with this incomplete set regresses users who right-click an existing OGG/AMR audio file in the vault; those files can no longer be added to the Local Files playlist or played through the local-file flow.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 1-line follow-up: PLAYABLE_EXTENSIONS now includes ogg and amr (and stays in sync with detectAudioFileExtension / getExtensionFromContentType), so right-clicking those files offers "Play with PodNotes" again while still suppressing the menu on non-audio files.
…files docs Review follow-up (PR #184). The helper no longer does any async work after the blob->getResourcePath change, so drop the now-misleading `async`/Promise wrapping and return a plain string; the awaiting call site is simplified accordingly. Also reword the local-files docs to describe the native resource-path mechanism rather than asserting unverified cross-platform coverage.
Review summary — adversarial + ultracodeRan two independent review tracks on this implementation:
Addressed in
|
The new extension allow-list was stricter than the (buggy match-everything) regex it replaced, so right-clicking an .ogg or .amr file no longer offered "Play with PodNotes" — a regression for formats PodNotes already recognizes as audio via detectAudioFileExtension and getExtensionFromContentType. Align the set with those sources.
# [2.17.0](2.16.0...2.17.0) (2026-06-22) ### Bug Fixes * behavioral-audit logic and robustness fixes (back-end, 1/2) ([#213](#213)) ([4e2845d](4e2845d)) * **download:** default per-episode download path and migrate empty default ([#183](#183)) ([#186](#186)) ([46a6486](46a6486)) * **download:** prevent Android crash and create missing folders on download ([#178](#178)) ([ecd09d5](ecd09d5)), closes [#113](#113) [#86](#86) [#113](#113) [#86](#86) * **lifecycle:** defer mobile podcast view startup ([#208](#208)) ([8bf9ce4](8bf9ce4)) * **notes:** cap note path length and harden folder creation ([#22](#22), [#87](#87)) ([#192](#192)) ([858e280](858e280)), closes [#87-class](#87) * **playback:** persist listened time during playback ([#33](#33)) ([#190](#190)) ([e3433c2](e3433c2)), closes [#191](#191) [#108](#108) [#163](#163) [#183](#183) * **playback:** play local files and downloads on iOS via resource path ([#100](#100)) ([#184](#184)) ([12c503a](12c503a)) * **player:** clear progress on episode switch to stop end-of-playback glitch ([#94](#94)) ([#194](#194)) ([59dccb3](59dccb3)) * **player:** reveal PodNotes view on Play with PodNotes so local files play ([#84](#84)) ([#198](#198)) ([5953625](5953625)) * **settings:** show labelled Add/Remove buttons in podcast search ([#109](#109)) ([#195](#195)) ([33399f5](33399f5)) * show downloaded episodes in the Local Files playlist ([#176](#176)) ([#177](#177)) ([184188c](184188c)) * **timestamps:** capture into the cursor's table cell without breaking the row ([#165](#165)) ([#203](#203)) ([964e342](964e342)) * **transcription:** always transcribe the currently playing episode ([#182](#182)) ([62b488a](62b488a)), closes [#107](#107) * **uri:** preserve '+' in episode titles and paths for timestamp links ([#181](#181)) ([8ad7aa5](8ad7aa5)), closes [#164](#164) * **view:** reliably reveal PodNotes view via command + ribbon icon ([#55](#55)) ([#199](#199)) ([fa1d708](fa1d708)) ### Features * add podcast segment links ([#205](#205)) ([d97e59e](d97e59e)) * **api:** expose generated episode transcripts ([e264465](e264465)), closes [#105](#105) * behavioral-audit UI and interaction fixes (front-end, 2/2) ([#215](#215)) ([894d93d](894d93d)) * **commands:** add playback rate and media timestamp controls ([#206](#206)) ([a8bb44a](a8bb44a)) * **devx:** isolated per-worktree Obsidian E2E vault wrapper ([#188](#188)) ([a8b7a4a](a8b7a4a)) * **episodes:** add a setting to control the Latest Episodes list length ([#114](#114)) ([#200](#200)) ([7b3e3c6](7b3e3c6)) * **notes:** add {{episodelink}} template tag to resume an episode from its note ([#35](#35)) ([#193](#193)) ([8c1ddd6](8c1ddd6)) * **notes:** add podcast feed-level notes ([#163](#163)) ([#187](#187)) ([db0de47](db0de47)), closes [#161](#161) [#160](#160) * **notes:** ship a Bases-friendly default episode note template ([#160](#160)) ([#201](#201)) ([209431d](209431d)), closes [#163](#163) [#183](#183) * **player:** scale episode title font size to its length ([#81](#81)) ([#202](#202)) ([659b6b8](659b6b8)) * **player:** support video episode playback ([#209](#209)) ([f92a91f](f92a91f)) * **queue:** add setting to disable queue auto-population and auto-advance ([#108](#108)) ([#185](#185)) ([cf3d73c](cf3d73c)) * **queue:** allow reordering the playback queue ([#80](#80)) ([#179](#179)) ([d994d61](d994d61)), closes [#173](#173) * **settings:** import/export settings & templates ([#180](#180)) ([a27d23d](a27d23d)), closes [#162](#162) [#162](#162) * **templates:** add {{currentDate}}, {{episodeNumber}}, {{duration}} template variables ([#189](#189)) ([ec573eb](ec573eb)), closes [#75](#75) [#34](#34) [#88](#88) [163/#186](#186) * **templates:** add episode chapters tag ([#207](#207)) ([9c98863](9c98863)) * **transcripts:** opt-in speaker diarization for transcripts ([#168](#168)) ([#204](#204)) ([a96e12f](a96e12f))
|
🎉 This PR is included in version 2.17.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Local audio files and downloaded episodes could not be played on iOS — the player never started — while streaming worked (issue #100, filed Jul 2024).
Root cause. Both local files and downloaded episodes were played from in-memory
blob:URLs (URL.createObjectURL), while streaming used the rawhttps://URL. iOS's WKWebView<audio>element loads media out-of-process and cannot read an in-memoryblob:URL, so playback silently failed on iOS. Desktop (Electron/Chromium) playsblob:audio fine, which is why the bug was iOS-only and never reproduced on desktop.The player code path is identical for streaming vs. local/downloaded except for the value bound to
<audio src>—getSrc()returns a real URL for streaming (works on iOS) vs. a blob for local/downloaded (fails). That isolates the blob URL as the cause.Fix. Resolve vault files with
app.vault.getResourcePath(file)— the same native-served, byte-range-capable URL Obsidian uses for its own![[media]]embeds (which play on iOS). The change lives in the sharedcreateMediaUrlObjectFromFilePathhelper, so both call sites (getContextMenuHandlerandEpisodePlayer.getSrc) are fixed with no player-UI changes. It also stops reading the entire audio file into memory as anArrayBufferjust to play it.Changes
src/utility/createMediaUrlObjectFromFilePath.ts— blob URL →getResourcePath; removed the now-deadBlobUrlManager.src/main.ts— removed theblobUrlManagerimport +revokeAll()unload wiring (its only consumer).src/getContextMenuHandler.ts— fixed the "Play with PodNotes" extension check: the inline regex/mp3|…|/had a trailing empty alternative that matched every file, so the menu item appeared on all files. Replaced with an explicitPLAYABLE_EXTENSIONSset.src/utility/createMediaUrlObjectFromFilePath.test.ts— new unit tests (forwards resource URL;""for missing/non-file; non-mp3 extensions).docs/docs/local_files.md— note that local/downloaded files play on all platforms.Verification
getResourcePathplay (loadedmetadata,duration: 3,readyState: 4) — confirming desktop cannot reproduce the iOS-only failure and that the resource URL is a valid, playable source.lint,format:check,typecheck,build,test(248 passed / 27 files, incl.EpisodePlayer.test.ts) all green.docs:buildnot run locally (mkdocsunavailable in the environment; change is one prose line, no nav/reference changes).This fix is reasoned and desktop-validated but not device-proven. The exact URL
getResourcePathreturns on iOS could not be confirmed here (the typings documentresourcePathPrefixasfile:///on mobile). It is the same API behind Obsidian's own iOS-working media embeds, so it is very likely correct — but before relying on it, please confirm on a real iPhone: logapp.vault.getResourcePath(tfile)for a local audio file and verify<audio>firesloadedmetadata(readyState >= 2).Closes #100