Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aec38b760 | ||
|
|
0187ee179e |
@@ -9,6 +9,19 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
|||||||
For how long each fixed defect had been shipping before it was found, see
|
For how long each fixed defect had been shipping before it was found, see
|
||||||
[docs/defect-windows.md](docs/defect-windows.md).
|
[docs/defect-windows.md](docs/defect-windows.md).
|
||||||
|
|
||||||
|
## v0.11.2
|
||||||
|
|
||||||
|
### 🐛 Fixes
|
||||||
|
|
||||||
|
- **Subtitles appear on screen on Android.** Turning one on did nothing, even
|
||||||
|
once they were loading again: the player hands finished subtitles to a view
|
||||||
|
that draws them, and on the native Android path there was no such view — so
|
||||||
|
every cue was decoded, delivered and dropped. There is one now, sitting over
|
||||||
|
the picture and under the controls, following the video's shape when the
|
||||||
|
screen turns. This was hidden behind the loading failure fixed in v0.11.1;
|
||||||
|
with nothing to select, there had never been a cue to lose. (UR-020, UR-003 →
|
||||||
|
DR-260)
|
||||||
|
|
||||||
## v0.11.1
|
## v0.11.1
|
||||||
|
|
||||||
Four fixes. Two had been present since the first release and were found on a
|
Four fixes. Two had been present since the first release and were found on a
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ Internal architecture, components, and application logic.
|
|||||||
| DR-257 | A container's children are ordered by **what the container is**, decided in Rust. The frontend pinned `SortBy=SortName` onto every drill-down, so a Jellypod podcast — a Jellyfin channel folder whose plugin returns episodes newest-first and prefixes played ones with "[Played]" — listed alphabetically, which both discarded the release order and clumped every heard episode at the top. `ChannelFolderItem` with `is_folder` now maps to its own `MediaKind::ChannelFolder` rather than collapsing into `Folder`, which is what makes the two distinguishable at all; `default_listing_sort` maps that kind to `PremiereDate` descending and every other container to `SortName` ascending, and a caller that names no container still gets no `SortBy`, so paths relying on the server's own order (a playlist's stored order) keep it. An explicit sort always wins. The offline leg of the cache/server race applies the same order, so the cached list does not flash in name order before the server's arrives. The store now names the container and never a sort field — the ordering rule is domain vocabulary, the same division as `SearchScope` | Repository | UR-007 | Done |
|
| DR-257 | A container's children are ordered by **what the container is**, decided in Rust. The frontend pinned `SortBy=SortName` onto every drill-down, so a Jellypod podcast — a Jellyfin channel folder whose plugin returns episodes newest-first and prefixes played ones with "[Played]" — listed alphabetically, which both discarded the release order and clumped every heard episode at the top. `ChannelFolderItem` with `is_folder` now maps to its own `MediaKind::ChannelFolder` rather than collapsing into `Folder`, which is what makes the two distinguishable at all; `default_listing_sort` maps that kind to `PremiereDate` descending and every other container to `SortName` ascending, and a caller that names no container still gets no `SortBy`, so paths relying on the server's own order (a playlist's stored order) keep it. An explicit sort always wins. The offline leg of the cache/server race applies the same order, so the cached list does not flash in name order before the server's arrives. The store now names the container and never a sort field — the ordering rule is domain vocabulary, the same division as `SearchScope` | Repository | UR-007 | Done |
|
||||||
| DR-258 | An audio-track change is honoured by **re-opening the stream** when the stream cannot carry the track. Jellyfin builds a transcode around one `AudioStreamIndex`, so the alternate tracks are not in it — but the native path only ever called `setAudioTrack(n)`, which indexes ExoPlayer's audio track *groups*. On Android that is the common case, since any source whose default audio codec the device cannot decode is transcoded: ExoPlayer held one audio track while the menu listed every track in the file, so every selection warned `Invalid audio track index` and was dropped, leaving the default track playing with nothing in the UI saying so. `determine_audio_track_switch_strategy` now decides by whether the stream in front of the engine carries the track at all — a direct play still selects in place, a transcode is re-negotiated at the chosen index and resumed. Where it resumes is the player's answer, not the UI's: the native path has no `<video>` element to read, so it sends no position, and defaulting that to zero re-opened the film at the beginning | Player | UR-021, UR-005 | Done |
|
| DR-258 | An audio-track change is honoured by **re-opening the stream** when the stream cannot carry the track. Jellyfin builds a transcode around one `AudioStreamIndex`, so the alternate tracks are not in it — but the native path only ever called `setAudioTrack(n)`, which indexes ExoPlayer's audio track *groups*. On Android that is the common case, since any source whose default audio codec the device cannot decode is transcoded: ExoPlayer held one audio track while the menu listed every track in the file, so every selection warned `Invalid audio track index` and was dropped, leaving the default track playing with nothing in the UI saying so. `determine_audio_track_switch_strategy` now decides by whether the stream in front of the engine carries the track at all — a direct play still selects in place, a transcode is re-negotiated at the chosen index and resumed. Where it resumes is the player's answer, not the UI's: the native path has no `<video>` element to read, so it sends no position, and defaulting that to zero re-opened the film at the beginning | Player | UR-021, UR-005 | Done |
|
||||||
| DR-259 | Subtitle URLs address Jellyfin's route, `/Videos/{item}/{source}/Subtitles/{index}/Stream.{format}`. The `Stream.` segment was missing, which matches no route and 404s, so every sideloaded subtitle failed to fetch. Since media3 1.5 a sideloaded text track only becomes a track group once its file is parsed, so 42 failed fetches left ExoPlayer with no text tracks at all and subtitle selection warned `available: 0` and did nothing. The URL tests that existed asserted the shape of a *mock helper* duplicating the format string rather than the URL the app requests, which is why a route error survived from the first release | Repository | UR-020 | Done |
|
| DR-259 | Subtitle URLs address Jellyfin's route, `/Videos/{item}/{source}/Subtitles/{index}/Stream.{format}`. The `Stream.` segment was missing, which matches no route and 404s, so every sideloaded subtitle failed to fetch. Since media3 1.5 a sideloaded text track only becomes a track group once its file is parsed, so 42 failed fetches left ExoPlayer with no text tracks at all and subtitle selection warned `available: 0` and did nothing. The URL tests that existed asserted the shape of a *mock helper* duplicating the format string rather than the URL the app requests, which is why a route error survived from the first release | Repository | UR-020 | Done |
|
||||||
|
| DR-260 | Subtitle cues are **drawn**. ExoPlayer decodes subtitles and delivers them to a listener; it draws none of them itself, and native video here is a bare `TextureView` the WebView composites over rather than a `PlayerView`, so nothing was holding the cues and a selected track rendered nowhere. The gap was invisible while every subtitle URL 404ed (DR-259) — with no text track to select there was never a cue to drop, so fixing the URL is what exposed it. `media3-ui`'s `SubtitleView` now takes each `CueGroup` from `onCues` and is attached at index 1 of the content view: above the video, still below the WebView, so cues sit over the picture and under the app's own controls. It is fitted to the letterboxed video rect rather than the screen, so cues stay inside the picture and follow it on rotation, and is torn down with the surface it belongs to. Verified on a device | Player | UR-020, UR-003 | Done |
|
||||||
| DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer — through a future `{@html}`, a dependency, or a devtools paste — would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` — a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `<style>` element survives into `index.html`, since a nonce there would make Tauri's injection outrank — and therefore void — `'unsafe-inline'`. `worker-src blob:` and `media-src blob:` are hls.js: it demuxes in a worker built from a blob and attaches MSE through `URL.createObjectURL`. `asset:` and `http://asset.localhost` are the same protocol under the two naming schemes `convertFileSrc` emits (custom scheme on Linux/macOS, `http` host on Windows/Android); `ipc:`/`http://ipc.localhost` is the invoke transport, which would otherwise be blocked by `connect-src`. A run-time CSP naming the server origin exactly was rejected: Tauri computes the header from immutable config when it serves the HTML, so it would mean rebuilding config and reloading the webview on every server change, for a policy the user can already point anywhere. The asset-protocol scope narrows from `$APPDATA/**` to `$APPDATA/thumbnails/**` — since DR-137 moved downloaded media to the loopback server, `imageCache` is the only `convertFileSrc` caller left, so the database and the encrypted-token fallback file no longer sit inside the grant | Security | UR-012, UR-071 | Done |
|
| DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer — through a future `{@html}`, a dependency, or a devtools paste — would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` — a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `<style>` element survives into `index.html`, since a nonce there would make Tauri's injection outrank — and therefore void — `'unsafe-inline'`. `worker-src blob:` and `media-src blob:` are hls.js: it demuxes in a worker built from a blob and attaches MSE through `URL.createObjectURL`. `asset:` and `http://asset.localhost` are the same protocol under the two naming schemes `convertFileSrc` emits (custom scheme on Linux/macOS, `http` host on Windows/Android); `ipc:`/`http://ipc.localhost` is the invoke transport, which would otherwise be blocked by `connect-src`. A run-time CSP naming the server origin exactly was rejected: Tauri computes the header from immutable config when it serves the HTML, so it would mean rebuilding config and reloading the webview on every server change, for a policy the user can already point anywhere. The asset-protocol scope narrows from `$APPDATA/**` to `$APPDATA/thumbnails/**` — since DR-137 moved downloaded media to the loopback server, `imageCache` is the only `convertFileSrc` caller left, so the database and the encrypted-token fallback file no longer sit inside the grant | Security | UR-012, UR-071 | Done |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jellytau",
|
"name": "jellytau",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"description": "A cross-platform Jellyfin client built with Tauri, SvelteKit and Rust.",
|
"description": "A cross-platform Jellyfin client built with Tauri, SvelteKit and Rust.",
|
||||||
"author": "Duncan Tourolle <duncan@tourolle.paris>",
|
"author": "Duncan Tourolle <duncan@tourolle.paris>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
Generated
+1
-1
@@ -2181,7 +2181,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jellytau"
|
name = "jellytau"
|
||||||
version = "0.11.1"
|
version = "0.11.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name = "jellytau"
|
|||||||
# `player-conformance`, and a second binary makes a bare `cargo run` —
|
# `player-conformance`, and a second binary makes a bare `cargo run` —
|
||||||
# which `tauri dev` issues — ambiguous.
|
# which `tauri dev` issues — ambiguous.
|
||||||
default-run = "jellytau"
|
default-run = "jellytau"
|
||||||
version = "0.11.1"
|
version = "0.11.2"
|
||||||
description = "A cross-platform Jellyfin client"
|
description = "A cross-platform Jellyfin client"
|
||||||
authors = ["Duncan Tourolle <duncan@tourolle.paris>"]
|
authors = ["Duncan Tourolle <duncan@tourolle.paris>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ dependencies {
|
|||||||
implementation("androidx.media3:media3-exoplayer-hls:1.5.0")
|
implementation("androidx.media3:media3-exoplayer-hls:1.5.0")
|
||||||
implementation("androidx.media3:media3-session:1.5.0")
|
implementation("androidx.media3:media3-session:1.5.0")
|
||||||
implementation("androidx.media3:media3-common:1.5.0")
|
implementation("androidx.media3:media3-common:1.5.0")
|
||||||
|
// SubtitleView. ExoPlayer delivers cues to a listener and draws none of them
|
||||||
|
// itself: without a view to hand them to, a selected subtitle track renders
|
||||||
|
// nowhere. See JellyTauPlayer.onCues. (DR-260)
|
||||||
|
implementation("androidx.media3:media3-ui:1.5.0")
|
||||||
implementation("com.google.guava:guava:33.0.0-android")
|
implementation("com.google.guava:guava:33.0.0-android")
|
||||||
|
|
||||||
// Media library for VolumeProviderCompat (remote volume control)
|
// Media library for VolumeProviderCompat (remote volume control)
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import com.dtourolle.jellytau.player.JellyTauPlayer
|
|||||||
object VideoOverlayManager {
|
object VideoOverlayManager {
|
||||||
|
|
||||||
private var attachedSurfaceView: TextureView? = null
|
private var attachedSurfaceView: TextureView? = null
|
||||||
|
|
||||||
|
/** The cue view attached alongside it, removed by the same teardown. */
|
||||||
|
private var attachedSubtitleView: androidx.media3.ui.SubtitleView? = null
|
||||||
private var contentLayoutListener: android.view.View.OnLayoutChangeListener? = null
|
private var contentLayoutListener: android.view.View.OnLayoutChangeListener? = null
|
||||||
private var listenerContentView: ViewGroup? = null
|
private var listenerContentView: ViewGroup? = null
|
||||||
|
|
||||||
@@ -56,6 +59,25 @@ object VideoOverlayManager {
|
|||||||
contentView.addView(surfaceView, 0, layoutParams)
|
contentView.addView(surfaceView, 0, layoutParams)
|
||||||
attachedSurfaceView = surfaceView
|
attachedSurfaceView = surfaceView
|
||||||
|
|
||||||
|
// Subtitles go directly above the video and still below the WebView:
|
||||||
|
// visible through the transparent page, and under the app's own
|
||||||
|
// controls rather than over them. Index 1 is what makes that
|
||||||
|
// sandwich — the same reason the video is pinned to index 0.
|
||||||
|
// TRACES: UR-020, UR-003 | DR-260
|
||||||
|
player.getSubtitleView()?.let { subtitles ->
|
||||||
|
(subtitles.parent as? ViewGroup)?.removeView(subtitles)
|
||||||
|
contentView.addView(
|
||||||
|
subtitles,
|
||||||
|
1,
|
||||||
|
FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
attachedSubtitleView = subtitles
|
||||||
|
android.util.Log.d("VideoOverlayManager", "Subtitle view attached above the video")
|
||||||
|
}
|
||||||
|
|
||||||
// Re-fit the video whenever the content view's bounds change (e.g. on
|
// Re-fit the video whenever the content view's bounds change (e.g. on
|
||||||
// device rotation) so the video is letterboxed to fit instead of being
|
// device rotation) so the video is letterboxed to fit instead of being
|
||||||
// stretched/cropped by the MATCH_PARENT surface.
|
// stretched/cropped by the MATCH_PARENT surface.
|
||||||
@@ -101,6 +123,10 @@ object VideoOverlayManager {
|
|||||||
fun detachVideoSurface() {
|
fun detachVideoSurface() {
|
||||||
try {
|
try {
|
||||||
removeLayoutListener()
|
removeLayoutListener()
|
||||||
|
attachedSubtitleView?.let { subtitles ->
|
||||||
|
(subtitles.parent as? ViewGroup)?.removeView(subtitles)
|
||||||
|
attachedSubtitleView = null
|
||||||
|
}
|
||||||
attachedSurfaceView?.let { surfaceView ->
|
attachedSurfaceView?.let { surfaceView ->
|
||||||
(surfaceView.parent as? ViewGroup)?.removeView(surfaceView)
|
(surfaceView.parent as? ViewGroup)?.removeView(surfaceView)
|
||||||
attachedSurfaceView = null
|
attachedSurfaceView = null
|
||||||
|
|||||||
@@ -233,6 +233,23 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
/** The Surface handed to ExoPlayer, owned here rather than by the player. */
|
/** The Surface handed to ExoPlayer, owned here rather than by the player. */
|
||||||
private var videoSurface: android.view.Surface? = null
|
private var videoSurface: android.view.Surface? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws subtitle cues over the picture.
|
||||||
|
*
|
||||||
|
* ExoPlayer decodes subtitles and *delivers* them to a listener; it draws
|
||||||
|
* none of them itself. A `PlayerView` would supply this view, but native
|
||||||
|
* video here is a bare TextureView the WebView composites over, so nothing
|
||||||
|
* was holding the cues and a selected subtitle track rendered nowhere. That
|
||||||
|
* gap was invisible for as long as every subtitle URL 404ed (DR-259) — with
|
||||||
|
* no text track to select, there was never a cue to drop.
|
||||||
|
*
|
||||||
|
* Sized and positioned to the video rect rather than the screen, so cues sit
|
||||||
|
* inside the picture rather than in a letterbox bar.
|
||||||
|
*
|
||||||
|
* TRACES: UR-020, UR-003 | DR-260
|
||||||
|
*/
|
||||||
|
private var subtitleView: androidx.media3.ui.SubtitleView? = null
|
||||||
|
|
||||||
/** Last reported video frame size, used to fit the surface to the screen preserving aspect ratio */
|
/** Last reported video frame size, used to fit the surface to the screen preserving aspect ratio */
|
||||||
private var videoWidth: Int = 0
|
private var videoWidth: Int = 0
|
||||||
private var videoHeight: Int = 0
|
private var videoHeight: Int = 0
|
||||||
@@ -539,6 +556,19 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hand each cue group to the view that draws it.
|
||||||
|
*
|
||||||
|
* Fires with an empty list when subtitles are turned off or the
|
||||||
|
* track has nothing to show at this moment, which is what clears the
|
||||||
|
* previous cue — so this is the whole of both showing and hiding.
|
||||||
|
*
|
||||||
|
* TRACES: UR-020 | DR-260
|
||||||
|
*/
|
||||||
|
override fun onCues(cueGroup: androidx.media3.common.text.CueGroup) {
|
||||||
|
subtitleView?.setCues(cueGroup.cues)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||||
android.util.Log.d("JellyTauPlayer", "▶▶▶ AUDIO SESSION ID CHANGED: $audioSessionId")
|
android.util.Log.d("JellyTauPlayer", "▶▶▶ AUDIO SESSION ID CHANGED: $audioSessionId")
|
||||||
// ExoPlayer rebuilt its audio sink (e.g. on a format change), so
|
// ExoPlayer rebuilt its audio sink (e.g. on a format change), so
|
||||||
@@ -1294,9 +1324,32 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
}
|
}
|
||||||
android.util.Log.d("JellyTauPlayer", "Video TextureView created")
|
android.util.Log.d("JellyTauPlayer", "Video TextureView created")
|
||||||
}
|
}
|
||||||
|
if (subtitleView == null) {
|
||||||
|
subtitleView = androidx.media3.ui.SubtitleView(appContext).apply {
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
// Honour the viewer's system captioning preferences (size,
|
||||||
|
// colour, edge style) and whatever styling the track carries —
|
||||||
|
// the same defaults a PlayerView would have applied.
|
||||||
|
setUserDefaultStyle()
|
||||||
|
setUserDefaultTextSize()
|
||||||
|
}
|
||||||
|
android.util.Log.d("JellyTauPlayer", "SubtitleView created")
|
||||||
|
}
|
||||||
return videoView!!.hashCode()
|
return videoView!!.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view that draws subtitle cues, for VideoOverlayManager to attach
|
||||||
|
* directly above the video and below the WebView. Null before the first
|
||||||
|
* video load.
|
||||||
|
*/
|
||||||
|
fun getSubtitleView(): androidx.media3.ui.SubtitleView? {
|
||||||
|
return subtitleView
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the video view instance (for VideoOverlayManager).
|
* Get the video view instance (for VideoOverlayManager).
|
||||||
* Returns null if none has been created yet.
|
* Returns null if none has been created yet.
|
||||||
@@ -1413,6 +1466,20 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
lp.height = targetH
|
lp.height = targetH
|
||||||
view.layoutParams = lp
|
view.layoutParams = lp
|
||||||
view.requestLayout()
|
view.requestLayout()
|
||||||
|
|
||||||
|
// Cues belong to the picture, not to the screen: matching the
|
||||||
|
// letterboxed rect keeps them off the black bars and moves them
|
||||||
|
// with the video on rotation. (DR-260)
|
||||||
|
subtitleView?.let { subs ->
|
||||||
|
val slp = subs.layoutParams
|
||||||
|
if (slp is FrameLayout.LayoutParams) {
|
||||||
|
slp.gravity = android.view.Gravity.CENTER
|
||||||
|
}
|
||||||
|
slp.width = targetW
|
||||||
|
slp.height = targetH
|
||||||
|
subs.layoutParams = slp
|
||||||
|
subs.requestLayout()
|
||||||
|
}
|
||||||
android.util.Log.d(
|
android.util.Log.d(
|
||||||
"JellyTauPlayer",
|
"JellyTauPlayer",
|
||||||
"Video surface fitted to ${targetW}x${targetH} (video ${videoWidth}x${videoHeight}, avail ${availW}x${availH})"
|
"Video surface fitted to ${targetW}x${targetH} (video ${videoWidth}x${videoHeight}, avail ${availW}x${availH})"
|
||||||
@@ -1442,6 +1509,10 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
exoPlayer.clearVideoSurface()
|
exoPlayer.clearVideoSurface()
|
||||||
com.dtourolle.jellytau.VideoOverlayManager.detachVideoSurface()
|
com.dtourolle.jellytau.VideoOverlayManager.detachVideoSurface()
|
||||||
videoView = null
|
videoView = null
|
||||||
|
// Released with the surface it belonged to; detachVideoSurface
|
||||||
|
// removes it from the hierarchy, and keeping the reference would
|
||||||
|
// leave the next video's cues going to an orphaned view.
|
||||||
|
subtitleView = null
|
||||||
android.util.Log.d("JellyTauPlayer", "Video surface cleared and detached")
|
android.util.Log.d("JellyTauPlayer", "Video surface cleared and detached")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "JellyTau",
|
"productName": "JellyTau",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"identifier": "com.dtourolle.jellytau",
|
"identifier": "com.dtourolle.jellytau",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user