[PR #18] [MERGED] Stream sync #1369

Closed
opened 2026-04-24 23:58:00 +02:00 by adam · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/advplyr/audiobookshelf-app/pull/18
Author: @svdztn
Created: 10/26/2021
Status: Merged
Merged: 10/26/2021
Merged by: @advplyr

Base: masterHead: stream-sync


📝 Commits (7)

  • 44a782b restore from App “stop" & "destroyed", get data from player, avoid progress loss
  • 1a86621 native plugin: add getStreamSyncData()
  • be02219 native plugin: change setMediaSource() to the api with startPositionMs, avoid call output-0.ts (will cause server retranscoding)
  • 81316c7 add auto seekback
  • 9a684aa Merge branch 'advplyr:master' into stream-sync
  • a08b09f del debug log
  • 2ba41fe fix min pause time to 3000 ms

📊 Changes

4 files changed (+120 additions, -15 deletions)

View changed files

📝 android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt (+20 -0)
📝 android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt (+41 -3)
📝 components/AudioPlayerMini.vue (+54 -8)
📝 components/app/StreamContainer.vue (+5 -4)

📄 Description

progress skipping backwards after open the main app while playing

I have been tracking "play in stream mode, progress skipping backwards" issue for some time.
According to my test, there are 2 conditions.

1.

The first one is pressing the system backButton, the log shows "App destroyed", in this condition, only player is running, progress can not sync to server.

D/Capacitor: App paused
D/Capacitor: App stopped
D/Capacitor: App destroyed

2.

Another one is more subtle, maybe caused by my os I think. My os is MIUI12.5 (Android 11). When the screen locked and play audiobooks in stream mode for a period of time, it seems that the app will be hibernated by the os.
According to the log, the app can sync CurrentTime to server at the beginning. But the playInterval will stop after about 10 minutes.

D/Capacitor: App paused
D/Capacitor: App stopped
D/Capacitor: Saving instance state!
D/PlayerService: EVENT IS PLAYING CHANGED
V/Capacitor/MyNativeAudio: Notifying listeners for event onPlayingUpdate
D/PlayerService: GET SUPORTED ACITONS
D/PlayerService: Notification Posted 10 - Start Foreground | Notification(channel=audiobookshelf_channel shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0xa color=0x00000000 category=transport actions=3 vis=PUBLIC)
D/AudioTrack: getTimestamp_l(3551): device stall time corrected using current time 446958880756319
D/AudioTrack: getTimestamp_l(3551): stale timestamp time corrected, currentTimeNanos: 446953808877988 < limitNs: 446958782885798 < mStartNs: 446958862885798
W/AudioTrack: getTimestamp_l(3551): retrograde timestamp time corrected, 446958782885798 < 446959084843455
D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 25/16321 (fetch/transfer)
V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265582, pluginId: MyNativeAudio, methodName: getCurrentTime
V/Capacitor: callback: 65265582, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {}
D/MyNativeAudio: Get Current Time 233569
V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265583, pluginId: MyNativeAudio, methodName: getCurrentTime
V/Capacitor: callback: 65265583, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {}
D/MyNativeAudio: Get Current Time 234571
V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265584, pluginId: MyNativeAudio, methodName: getCurrentTime
V/Capacitor: callback: 65265584, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {}
D/MyNativeAudio: Get Current Time 235572
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4

...

V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265874, pluginId: MyNativeAudio, methodName: getCurrentTime
V/Capacitor: callback: 65265874, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {}
D/MyNativeAudio: Get Current Time 525525
D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/38763 (fetch/transfer)
D/PlayerService: EVENT_IS_LOADING_CHANGED : true
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4
D/PlayerService: EVENT_IS_LOADING_CHANGED : false
V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265875, pluginId: MyNativeAudio, methodName: getCurrentTime
V/Capacitor: callback: 65265875, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {}
D/MyNativeAudio: Get Current Time 526526
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4
D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/39165 (fetch/transfer)
D/PlayerService: EVENT_IS_LOADING_CHANGED : true
D/PlayerService: EVENT_IS_LOADING_CHANGED : false
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4
D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/39567 (fetch/transfer)
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4
D/PlayerService: EVENT_IS_LOADING_CHANGED : true
D/PlayerService: EVENT_IS_LOADING_CHANGED : false
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4
D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/39967 (fetch/transfer)
D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4]
D/PipelineWatcher: DEBUG: elapsed 0 / 4
D/PlayerService: EVENT_IS_LOADING_CHANGED : true
D/PlayerService: EVENT_IS_LOADING_CHANGED : false

Of course, we can the listening for the backButton and disable the default behaviour in the 1st condition, but I think it is not necessary to.

My methods is when the app runs again, read status and data from the player, and restore these data to the app after some processing. Such as do not initPlayer if the audiobook is the same one and the player is in playing.
I have been testing this code for a while, it worked fine.

other improvements

fix bug: player init cause stream retranscoding

The init progress is call mPlayer.setMediaSource() and prepare() first, when the state turns to READY, then call seekTo() to jump to the startTime. That will call the server for the file output-0.ts.

add auto seekback

I mentioned this in #12, when playing the paused audiobook again, seekback a bit of time can help user recall where they were last stopped.

There are 2 conditions too:
One is the player is still running, and no audiobook switch. The seekback is handled by the player which record the pauseTime when paused and calc seekback time when start to play.

Another one is switch audiobook or restart the app (player). Becase there is no data for the player to calc, so there need help from the server. We need the lastUpdate time of the audiobook, use it as the last paused time. The seekback is handled in the app, before player init.

And there is another problem, the seekback may cause stream retranscoding in the same way. So the server should transcode a little more befroe the clientCurrentTime at the beginning.

I will open a pull request to the server code at the same time.

P.S.

The rules of seekback for now :

  • totalPauseTime < 3s, seekback = 0
  • 3s <= totalPauseTime < 1m, seekback = totalPauseTime / 6
  • 1m <= totalPauseTime < 5m, seekback = 15s
  • 5m <= totalPauseTime < 30m, seekback = 20s
  • 30m <= totalPauseTime < 1h, seekback = 25s
  • 1h <= totalPauseTime, seekback = 29.5s

We can discuss whether there is a more suitable solution.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/advplyr/audiobookshelf-app/pull/18 **Author:** [@svdztn](https://github.com/svdztn) **Created:** 10/26/2021 **Status:** ✅ Merged **Merged:** 10/26/2021 **Merged by:** [@advplyr](https://github.com/advplyr) **Base:** `master` ← **Head:** `stream-sync` --- ### 📝 Commits (7) - [`44a782b`](https://github.com/advplyr/audiobookshelf-app/commit/44a782b6591be4b3212086405ef814740ddc403e) restore from App “stop" & "destroyed", get data from player, avoid progress loss - [`1a86621`](https://github.com/advplyr/audiobookshelf-app/commit/1a866210a28ffa6e2fda1acd28168b284a1ca0b2) native plugin: add getStreamSyncData() - [`be02219`](https://github.com/advplyr/audiobookshelf-app/commit/be02219f1a6ad6004fb3b42d2209d738ca827edd) native plugin: change setMediaSource() to the api with startPositionMs, avoid call output-0.ts (will cause server retranscoding) - [`81316c7`](https://github.com/advplyr/audiobookshelf-app/commit/81316c73c8c255c4fa8f57aa8eb123e789ecf9ef) add auto seekback - [`9a684aa`](https://github.com/advplyr/audiobookshelf-app/commit/9a684aa8404995ba299b00fd05b7717a69e7851c) Merge branch 'advplyr:master' into stream-sync - [`a08b09f`](https://github.com/advplyr/audiobookshelf-app/commit/a08b09f9a9c7a203ac8c9187f4fd4a0679ad9bbf) del debug log - [`2ba41fe`](https://github.com/advplyr/audiobookshelf-app/commit/2ba41fe47cd990a70da0234f45d514dbf3975fee) fix min pause time to 3000 ms ### 📊 Changes **4 files changed** (+120 additions, -15 deletions) <details> <summary>View changed files</summary> 📝 `android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt` (+20 -0) 📝 `android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt` (+41 -3) 📝 `components/AudioPlayerMini.vue` (+54 -8) 📝 `components/app/StreamContainer.vue` (+5 -4) </details> ### 📄 Description ## progress skipping backwards after open the main app while playing I have been tracking "play in stream mode, progress skipping backwards" issue for some time. According to my test, there are 2 conditions. ### 1. The first one is pressing the system backButton, the log shows "App destroyed", in this condition, only player is running, progress can not sync to server. ``` D/Capacitor: App paused D/Capacitor: App stopped D/Capacitor: App destroyed ``` ### 2. Another one is more subtle, maybe caused by my os I think. My os is MIUI12.5 (Android 11). When the screen locked and play audiobooks in stream mode for a period of time, it seems that the app will be hibernated by the os. According to the log, the app can sync `CurrentTime` to server at the beginning. But the `playInterval` will stop after about 10 minutes. ``` D/Capacitor: App paused D/Capacitor: App stopped D/Capacitor: Saving instance state! D/PlayerService: EVENT IS PLAYING CHANGED V/Capacitor/MyNativeAudio: Notifying listeners for event onPlayingUpdate D/PlayerService: GET SUPORTED ACITONS D/PlayerService: Notification Posted 10 - Start Foreground | Notification(channel=audiobookshelf_channel shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0xa color=0x00000000 category=transport actions=3 vis=PUBLIC) D/AudioTrack: getTimestamp_l(3551): device stall time corrected using current time 446958880756319 D/AudioTrack: getTimestamp_l(3551): stale timestamp time corrected, currentTimeNanos: 446953808877988 < limitNs: 446958782885798 < mStartNs: 446958862885798 W/AudioTrack: getTimestamp_l(3551): retrograde timestamp time corrected, 446958782885798 < 446959084843455 D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 25/16321 (fetch/transfer) V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265582, pluginId: MyNativeAudio, methodName: getCurrentTime V/Capacitor: callback: 65265582, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {} D/MyNativeAudio: Get Current Time 233569 V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265583, pluginId: MyNativeAudio, methodName: getCurrentTime V/Capacitor: callback: 65265583, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {} D/MyNativeAudio: Get Current Time 234571 V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265584, pluginId: MyNativeAudio, methodName: getCurrentTime V/Capacitor: callback: 65265584, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {} D/MyNativeAudio: Get Current Time 235572 D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 ... V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265874, pluginId: MyNativeAudio, methodName: getCurrentTime V/Capacitor: callback: 65265874, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {} D/MyNativeAudio: Get Current Time 525525 D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/38763 (fetch/transfer) D/PlayerService: EVENT_IS_LOADING_CHANGED : true D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 D/PlayerService: EVENT_IS_LOADING_CHANGED : false V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 65265875, pluginId: MyNativeAudio, methodName: getCurrentTime V/Capacitor: callback: 65265875, pluginId: MyNativeAudio, methodName: getCurrentTime, methodData: {} D/MyNativeAudio: Get Current Time 526526 D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/39165 (fetch/transfer) D/PlayerService: EVENT_IS_LOADING_CHANGED : true D/PlayerService: EVENT_IS_LOADING_CHANGED : false D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/39567 (fetch/transfer) D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 D/PlayerService: EVENT_IS_LOADING_CHANGED : true D/PlayerService: EVENT_IS_LOADING_CHANGED : false D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 D/BufferPoolAccessor2.0: bufferpool2 0xb4000078dd006428 : 4(32768 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 28/39967 (fetch/transfer) D/CCodecBufferChannel: [c2.android.mp3.decoder#87] DEBUG: elapsed: n=4 [in=0 pipeline=0 out=0 smoothness=4] D/PipelineWatcher: DEBUG: elapsed 0 / 4 D/PlayerService: EVENT_IS_LOADING_CHANGED : true D/PlayerService: EVENT_IS_LOADING_CHANGED : false ``` > Of course, we can the listening for the backButton and disable the default behaviour in the 1st condition, but I think it is not necessary to. My methods is when the app runs again, read status and data from the player, and restore these data to the app after some processing. Such as do not initPlayer if the audiobook is the same one and the player is in playing. I have been testing this code for a while, it worked fine. ## other improvements ### fix bug: player init cause stream retranscoding The init progress is call `mPlayer.setMediaSource()` and `prepare()` first, when the state turns to `READY`, then call `seekTo()` to jump to the startTime. That will call the server for the file `output-0.ts`. ### add auto seekback I mentioned this in #12, when playing the paused audiobook again, seekback a bit of time can help user recall where they were last stopped. There are 2 conditions too: One is the player is still running, and no audiobook switch. The `seekback` is handled by the player which record the pauseTime when paused and calc `seekback` time when start to play. Another one is switch audiobook or restart the app (player). Becase there is no data for the player to calc, so there need help from the server. We need the `lastUpdate time` of the audiobook, use it as the last paused time. The `seekback` is handled in the app, before player init. And there is another problem, the `seekback` may cause stream retranscoding in the same way. So the server should transcode a little more befroe the `clientCurrentTime` at the beginning. > I will open a pull request to the server code at the same time. #### P.S. > The rules of `seekback` for now : > - `totalPauseTime < 3s`, `seekback = 0` > - `3s <= totalPauseTime < 1m`, `seekback = totalPauseTime / 6` > - `1m <= totalPauseTime < 5m`, `seekback = 15s` > - `5m <= totalPauseTime < 30m`, `seekback = 20s` > - `30m <= totalPauseTime < 1h`, `seekback = 25s` > - `1h <= totalPauseTime`, `seekback = 29.5s` > > We can discuss whether there is a more suitable solution. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
adam added the pull-request label 2026-04-24 23:58:00 +02:00
adam closed this issue 2026-04-24 23:58:00 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf-app#1369