extended-vlc-player 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ Under the hood:
7
7
  | Platform | Engine | Default version | Min OS |
8
8
  | -------- | ----------------------------------- | --------------- | --------- |
9
9
  | iOS | [MobileVLCKit](https://code.videolan.org/videolan/VLCKit) | `~3.7.3` | iOS 16.4+ |
10
- | Android | [libVLC](https://code.videolan.org/videolan/vlc-android) | `3.6.0` | API 26+ |
10
+ | Android | [libVLC](https://code.videolan.org/videolan/vlc-android) | `3.7.5` | API 26+ |
11
11
 
12
12
  The JS surface intentionally mirrors `expo-video` so an existing `<VideoView>` consumer (e.g. `BackgroundVideoPlayer.jsx`) can be migrated with a one-line import swap.
13
13
 
@@ -185,7 +185,7 @@ Then in `app.json`:
185
185
  "plugins": [
186
186
  ["extended-vlc-player", {
187
187
  "ios": { "mobileVlcKitVersion": "3.7.3" },
188
- "android": { "libVlcVersion": "3.6.0" }
188
+ "android": { "libVlcVersion": "3.7.5" }
189
189
  }]
190
190
  ]
191
191
  ```
@@ -200,7 +200,7 @@ The plugin (`app.plugin.js`) does the following, idempotently:
200
200
 
201
201
  - **iOS Podfile** — adds `pod 'MobileVLCKit', '~> 3.7.3'` and a `post_install` hook that pins `IPHONEOS_DEPLOYMENT_TARGET = 16.4` on the MobileVLCKit target.
202
202
  - **iOS Info.plist** — adds `"audio"` to `UIBackgroundModes` so the audio session is eligible for background playback and PiP.
203
- - **Android `android/app/build.gradle`** — adds `implementation "org.videolan.android:libvlc:3.6.0"`. ABI filters are inherited from the host app.
203
+ - **Android `android/app/build.gradle`** — adds `implementation "org.videolan.android:libvlc-all:3.7.5"`. ABI filters are inherited from the host app.
204
204
  - **Android `AndroidManifest.xml`** — adds `android:supportsPictureInPicture="true"` and the `configChanges` set to `MainActivity`.
205
205
 
206
206
  To publish to npm, run `npm publish` from the module root. To consume from a local checkout, `file:` works during development and you should switch to the registry version before shipping (see [Troubleshooting](#troubleshooting)).
@@ -341,8 +341,9 @@ All options are optional. Defaults match what the plugin is pinned to in CI.
341
341
  enableBitcode: false,
342
342
  },
343
343
  android: {
344
- // org.videolan.android:libvlc version.
345
- libVlcVersion: '3.6.0',
344
+ // org.videolan.android:libvlc-all version. 3.7.5 ships 16 KB-aligned
345
+ // arm64 native binaries required by Android 15+ 16 KB devices.
346
+ libVlcVersion: '3.7.5',
346
347
  },
347
348
  pip: {
348
349
  // Reserved for the snapshot bridge. The current bridge runs at the
@@ -20,5 +20,5 @@ android {
20
20
  }
21
21
 
22
22
  dependencies {
23
- implementation 'org.videolan.android:libvlc-all:3.6.0'
23
+ implementation 'org.videolan.android:libvlc-all:3.7.5'
24
24
  }
@@ -2,12 +2,11 @@ package expo.modules.extendedvlcplayer
2
2
 
3
3
  import android.app.Activity
4
4
  import android.app.PictureInPictureParams
5
- import android.content.Context
6
- import android.content.res.Configuration
7
5
  import android.os.Build
8
6
  import android.util.Rational
9
7
  import expo.modules.kotlin.modules.Module
10
8
  import expo.modules.kotlin.modules.ModuleDefinition
9
+ import expo.modules.kotlin.records.Record
11
10
 
12
11
  /**
13
12
  * TurboModule for `extended-vlc-player` on Android.
@@ -22,6 +21,37 @@ class ExtendedVlcPlayerModule : Module() {
22
21
  override fun definition() = ModuleDefinition {
23
22
  Name("ExtendedVlcPlayer")
24
23
 
24
+ Function("createPlayer") {
25
+ val context = appContext.reactContext
26
+ ?: throw IllegalStateException("React context is not available")
27
+ PlayerRegistry.create(context).first
28
+ }
29
+
30
+ Function("destroyPlayer") { instanceId: Int ->
31
+ PlayerRegistry.destroy(instanceId)
32
+ }
33
+
34
+ View(ExtendedVlcPlayerViewComponentView::class) {
35
+ Name("ExtendedVlcPlayerView")
36
+ Events(
37
+ "onLoad",
38
+ "onProgress",
39
+ "onPlaying",
40
+ "onPaused",
41
+ "onEnded",
42
+ "onError",
43
+ "onBuffering",
44
+ "onPictureInPictureStart",
45
+ "onPictureInPictureStop"
46
+ )
47
+ Prop("player") { view: ExtendedVlcPlayerViewComponentView, id: Int ->
48
+ view.setPlayer(id)
49
+ }
50
+ Prop("contentFit") { view: ExtendedVlcPlayerViewComponentView, fit: String ->
51
+ view.setContentFit(fit)
52
+ }
53
+ }
54
+
25
55
  AsyncFunction("isPictureInPictureSupported") {
26
56
  Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
27
57
  appContext.currentActivity?.application?.packageManager?.hasSystemFeature(
@@ -96,4 +126,4 @@ class ExtendedVlcPlayerModule : Module() {
96
126
  class ReplacePayload(
97
127
  val uri: String,
98
128
  val instanceId: Int = 0
99
- )
129
+ ) : Record
@@ -1,7 +1,6 @@
1
1
  package expo.modules.extendedvlcplayer
2
2
 
3
3
  import android.content.Context
4
- import android.view.View
5
4
  import expo.modules.kotlin.AppContext
6
5
  import expo.modules.kotlin.viewevent.EventDispatcher
7
6
  import expo.modules.kotlin.views.ExpoView
@@ -17,6 +16,7 @@ class ExtendedVlcPlayerViewComponentView(context: Context, appContext: AppContex
17
16
  ExpoView(context, appContext) {
18
17
 
19
18
  private var playerId: Int = 0
19
+ private var contentFit: String = "contain"
20
20
 
21
21
  val onLoad by EventDispatcher<Map<String, Any>>()
22
22
  val onProgress by EventDispatcher<Map<String, Any>>()
@@ -29,11 +29,23 @@ class ExtendedVlcPlayerViewComponentView(context: Context, appContext: AppContex
29
29
  val onPictureInPictureStop by EventDispatcher<Unit>()
30
30
 
31
31
  fun setPlayer(id: Int) {
32
- if (playerId == id) return
33
- // If we were attached to a previous session, detach.
32
+ if (playerId == id) {
33
+ attachToSession()
34
+ return
35
+ }
34
36
  detachFromSession()
35
37
  playerId = id
36
- val session = PlayerRegistry.session(id) ?: return
38
+ attachToSession()
39
+ }
40
+
41
+ fun setContentFit(value: String) {
42
+ contentFit = value
43
+ }
44
+
45
+ private fun attachToSession() {
46
+ if (playerId == 0) return
47
+ val session = PlayerRegistry.session(playerId) ?: return
48
+ if (session.drawable.parent === this) return
37
49
  val drawable = session.drawable
38
50
  drawable.layoutParams = android.view.ViewGroup.LayoutParams(
39
51
  android.view.ViewGroup.LayoutParams.MATCH_PARENT,
@@ -53,12 +65,6 @@ class ExtendedVlcPlayerViewComponentView(context: Context, appContext: AppContex
53
65
  session.onPictureInPictureStop = { onPictureInPictureStop(Unit) }
54
66
  }
55
67
 
56
- fun createPlayer(): Int {
57
- val (id, _) = PlayerRegistry.create(context)
58
- playerId = id
59
- return id
60
- }
61
-
62
68
  private fun detachFromSession() {
63
69
  if (playerId == 0) return
64
70
  val session = PlayerRegistry.session(playerId) ?: return
@@ -76,7 +82,12 @@ class ExtendedVlcPlayerViewComponentView(context: Context, appContext: AppContex
76
82
  }
77
83
 
78
84
  override fun onDetachedFromWindow() {
79
- super.onDetachedFromWindow()
80
85
  detachFromSession()
86
+ super.onDetachedFromWindow()
87
+ }
88
+
89
+ override fun onAttachedToWindow() {
90
+ super.onAttachedToWindow()
91
+ attachToSession()
81
92
  }
82
93
  }
@@ -106,6 +106,7 @@ class PlayerSession(
106
106
  val media = Media(libVlc, Uri.parse(uri))
107
107
  currentMedia = media
108
108
  mediaPlayer.media = media
109
+ mediaPlayer.play()
109
110
  }
110
111
 
111
112
  private fun durationSeconds(): Double = mediaPlayer.getLength() / 1000.0
@@ -165,6 +166,10 @@ class PlayerSession(
165
166
  }
166
167
 
167
168
  fun release() {
169
+ val vout = mediaPlayer.getVLCVout()
170
+ if (vout.areViewsAttached()) {
171
+ vout.detachViews()
172
+ }
168
173
  mediaPlayer.stop()
169
174
  currentMedia?.release()
170
175
  libVlc.release()
package/app.plugin.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * Plugin options (all optional):
23
23
  * {
24
24
  * ios: { mobileVlcKitVersion: '3.7.3', enableBitcode: false },
25
- * android: { libVlcVersion: '3.6.0' },
25
+ * android: { libVlcVersion: '3.7.5' },
26
26
  * pip: { snapshotFps: 30, snapshotQuality: 'medium' },
27
27
  * }
28
28
  */
@@ -83,7 +83,7 @@ const GRADLE_TAG = 'extended-vlc-player-gradle';
83
83
  const GRADLE_VLC = (vlcVersion) => ` implementation "org.videolan.android:libvlc-all:${vlcVersion}"`;
84
84
 
85
85
  function withAndroidGradle(config, options) {
86
- const vlcVersion = (options?.android?.libVlcVersion || '3.6.0').toString();
86
+ const vlcVersion = (options?.android?.libVlcVersion || '3.7.5').toString();
87
87
  return withAppBuildGradle(config, (gradleConfig) => {
88
88
  if (gradleConfig.modResults.language !== 'groovy') {
89
89
  // The main app module is Groovy in this Expo template (verified in
@@ -93,15 +93,30 @@ function withAndroidGradle(config, options) {
93
93
  '[extended-vlc-player] Android build.gradle is not Groovy. Update the plugin to handle the new DSL.'
94
94
  );
95
95
  }
96
- const newSrc = mergeContents({
96
+
97
+ const currentContents = gradleConfig.modResults.contents;
98
+ const existingBlock = new RegExp(
99
+ `(// @generated begin ${GRADLE_TAG}\\b[\\s\\S]*?implementation\\s+[\"']org\\.videolan\\.android:libvlc(?:-all)?:)([^\"']+)([\"'])`,
100
+ 'm'
101
+ );
102
+ const updatedContents = currentContents.replace(
103
+ existingBlock,
104
+ (_match, prefix, _previousVersion, closingQuote) => `${prefix}${vlcVersion}${closingQuote}`
105
+ );
106
+
107
+ if (updatedContents !== currentContents) {
108
+ gradleConfig.modResults.contents = updatedContents;
109
+ return gradleConfig;
110
+ }
111
+
112
+ gradleConfig.modResults.contents = mergeContents({
97
113
  tag: GRADLE_TAG,
98
- src: gradleConfig.modResults.contents,
114
+ src: currentContents,
99
115
  newSrc: `\n${INFOPLIST_AUDIO_SESSION}\ndependencies {\n${GRADLE_VLC(vlcVersion)}\n}\n`,
100
116
  anchor: /^android\s*\{/m,
101
117
  offset: 1,
102
118
  comment: '//',
103
119
  }).contents;
104
- gradleConfig.modResults.contents = newSrc;
105
120
  return gradleConfig;
106
121
  });
107
122
  }
@@ -5,8 +5,8 @@ export interface UseExtendedVlcPlayerOptions {
5
5
  }
6
6
  /**
7
7
  * Returns a stable player object for the given source. The native player
8
- * is created once (lazy on first method call) and replaced in place when
9
- * `source` changes via the same `replace` API used by `expo-video`.
8
+ * is created once on mount and replaced in place when `source` changes via
9
+ * the same `replace` API used by `expo-video`.
10
10
  */
11
11
  export declare function useExtendedVlcPlayer(source: ExtendedVlcSource, options?: UseExtendedVlcPlayerOptions): ExtendedVlcPlayer;
12
12
  //# sourceMappingURL=useExtendedVlcPlayer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useExtendedVlcPlayer.d.ts","sourceRoot":"","sources":["../src/useExtendedVlcPlayer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAqCjB,MAAM,WAAW,2BAA2B;IAC1C,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,CAqGnB"}
1
+ {"version":3,"file":"useExtendedVlcPlayer.d.ts","sourceRoot":"","sources":["../src/useExtendedVlcPlayer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAqCpE,MAAM,WAAW,2BAA2B;IAC1C,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,CA0GnB"}
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { requireNativeModule } from 'expo-modules-core';
3
3
  // Native module is registered by the platform-specific module under the
4
4
  // JS module name "ExtendedVlcPlayer". The requireNativeModule helper resolves
@@ -34,8 +34,8 @@ function normalizeSource(source) {
34
34
  }
35
35
  /**
36
36
  * Returns a stable player object for the given source. The native player
37
- * is created once (lazy on first method call) and replaced in place when
38
- * `source` changes via the same `replace` API used by `expo-video`.
37
+ * is created once on mount and replaced in place when `source` changes via
38
+ * the same `replace` API used by `expo-video`.
39
39
  */
40
40
  export function useExtendedVlcPlayer(source, options = {}) {
41
41
  const normalized = useMemo(() => normalizeSource(source), [source]);
@@ -53,12 +53,36 @@ export function useExtendedVlcPlayer(source, options = {}) {
53
53
  isMountedRef.current = false;
54
54
  };
55
55
  }, []);
56
+ useEffect(() => {
57
+ let createdId = 0;
58
+ try {
59
+ createdId = getModule().createPlayer();
60
+ setNativeId(createdId);
61
+ }
62
+ catch (error) {
63
+ // eslint-disable-next-line no-console
64
+ console.warn('[extended-vlc-player] player creation failed:', error);
65
+ }
66
+ return () => {
67
+ if (createdId <= 0)
68
+ return;
69
+ try {
70
+ getModule().destroyPlayer(createdId);
71
+ }
72
+ catch (error) {
73
+ // eslint-disable-next-line no-console
74
+ console.warn('[extended-vlc-player] player cleanup failed:', error);
75
+ }
76
+ };
77
+ }, []);
56
78
  useEffect(() => {
57
79
  // When the source prop changes, ask the native module to swap in place
58
80
  // instead of remounting the view (which would reset audio session, PiP
59
81
  // delegate wiring, etc.).
82
+ if (nativeId <= 0)
83
+ return;
60
84
  try {
61
- getModule().replace(normalized);
85
+ getModule().replace(nativeId, normalized);
62
86
  }
63
87
  catch (error) {
64
88
  // Surface as console error; the player view will also emit onError
@@ -66,7 +90,7 @@ export function useExtendedVlcPlayer(source, options = {}) {
66
90
  // eslint-disable-next-line no-console
67
91
  console.warn('[extended-vlc-player] replace failed:', error);
68
92
  }
69
- }, [normalized]);
93
+ }, [nativeId, normalized]);
70
94
  // The player object is intentionally stable across renders. Methods are
71
95
  // closures over `nativeId` so the latest normalized source is always
72
96
  // referenced, but the object identity does not change — consumers can
@@ -85,12 +109,7 @@ export function useExtendedVlcPlayer(source, options = {}) {
85
109
  replace: (next) => {
86
110
  const nextNormalized = normalizeSource(next);
87
111
  try {
88
- getModule().replace({
89
- ...nextNormalized,
90
- // Tag the replace with the current nativeId so the existing
91
- // instance is updated rather than a new one allocated.
92
- instanceId: nativeId,
93
- });
112
+ getModule().replace(nativeId, nextNormalized);
94
113
  }
95
114
  catch (error) {
96
115
  // eslint-disable-next-line no-console
@@ -118,20 +137,9 @@ export function useExtendedVlcPlayer(source, options = {}) {
118
137
  }
119
138
  }
120
139
  }, [player, options]);
121
- // Bump the native id once on mount so the first source prop is committed
122
- // to the native side. We use a ref-based "first" instead of an effect to
123
- // avoid an extra render.
124
- const firstRef = useRef(true);
125
- useEffect(() => {
126
- if (firstRef.current) {
127
- firstRef.current = false;
128
- setNativeId(1);
129
- }
130
- }, []);
131
- // Mark unused variables to keep TypeScript happy with strict checks.
132
- void useCallback;
140
+ // Keep the ref in the hook for consumers that unmount while a native call
141
+ // is in flight; the native registry owns the actual session lifetime.
133
142
  void isMountedRef;
134
- void undefined;
135
143
  return player;
136
144
  }
137
145
  //# sourceMappingURL=useExtendedVlcPlayer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useExtendedVlcPlayer.js","sourceRoot":"","sources":["../src/useExtendedVlcPlayer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AASxD,wEAAwE;AACxE,8EAA8E;AAC9E,4EAA4E;AAC5E,0EAA0E;AAC1E,qCAAqC;AACrC,IAAI,YAAY,GAAQ,IAAI,CAAC;AAC7B,IAAI,CAAC;IACH,YAAY,GAAG,mBAAmB,CAAC,mBAAmB,CAAC,CAAC;AAC1D,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,0EAA0E;IAC1E,yEAAyE;IACzE,wDAAwD;IACxD,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC;AAED,SAAS,SAAS;IAChB,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,wDAAwD;YACtD,uDAAuD;YACvD,4CAA4C,CAC/C,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,eAAe,CAAC,MAAyB;IAChD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IACvD,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;IACvB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;AAC3E,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAyB,EACzB,UAAuC,EAAE;IAEzC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACpE,uEAAuE;IACvE,0EAA0E;IAC1E,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAElC,yEAAyE;IACzE,8DAA8D;IAC9D,qEAAqE;IACrE,8BAA8B;IAE9B,SAAS,CAAC,GAAG,EAAE;QACb,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,OAAO,GAAG,EAAE;YACV,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC;QAC/B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,uEAAuE;QACvE,uEAAuE;QACvE,0BAA0B;QAC1B,IAAI,CAAC;YACH,SAAS,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,mEAAmE;YACnE,gDAAgD;YAChD,sCAAsC;YACtC,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE,KAAK,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB,wEAAwE;IACxE,qEAAqE;IACrE,sEAAsE;IACtE,sCAAsC;IACtC,MAAM,MAAM,GAAG,OAAO,CAAoB,GAAG,EAAE;QAC7C,MAAM,CAAC,GAA8C;YACnD,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;YACtC,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;YACxC,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;YACtC,IAAI,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC;YAC9D,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;YAC9D,SAAS,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC;YACtE,aAAa,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC;YAC5E,gBAAgB,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC;YAClF,OAAO,EAAE,CAAC,IAAuB,EAAE,EAAE;gBACnC,MAAM,cAAc,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;gBAC7C,IAAI,CAAC;oBACH,SAAS,EAAE,CAAC,OAAO,CAAC;wBAClB,GAAG,cAAc;wBACjB,4DAA4D;wBAC5D,uDAAuD;wBACvD,UAAU,EAAE,QAAQ;qBACrB,CAAC,CAAC;gBACL,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,sCAAsC;oBACtC,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE,KAAK,CAAC,CAAC;gBAC/D,CAAC;YACH,CAAC;YACD,qBAAqB,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,qBAAqB,CAAC,QAAQ,CAAC;YACxE,oBAAoB,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC;YACtE,wBAAwB,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,wBAAwB,CAAC,QAAQ,CAAC;YAC9E,2BAA2B,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,2BAA2B,EAAE;SAC7E,CAAC;QACF,OAAO,CAAC,CAAC;QACT,uDAAuD;IACzD,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,uEAAuE;IACvE,qEAAqE;IACrE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,IAAI,CAAC;gBACH,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC1B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;YACtE,CAAC;QACH,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAEtB,yEAAyE;IACzE,yEAAyE;IACzE,yBAAyB;IACzB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC;YACzB,WAAW,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,qEAAqE;IACrE,KAAK,WAAW,CAAC;IACjB,KAAK,YAAY,CAAC;IAClB,KAAM,SAAyE,CAAC;IAEhF,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { requireNativeModule } from 'expo-modules-core';\n\nimport type {\n ExtendedVlcLoadEvent,\n ExtendedVlcPlayer,\n ExtendedVlcProgressEvent,\n ExtendedVlcSource,\n} from './types';\n\n// Native module is registered by the platform-specific module under the\n// JS module name \"ExtendedVlcPlayer\". The requireNativeModule helper resolves\n// it at runtime; if the native side is missing, accessing the module throws\n// — we surface a clear error so the consumer knows what failed instead of\n// silently producing a no-op player.\nlet nativeModule: any = null;\ntry {\n nativeModule = requireNativeModule('ExtendedVlcPlayer');\n} catch (error) {\n // Allow JS-side import of the module to succeed even when the native side\n // is not yet built (e.g. in a Jest test environment). We throw a helpful\n // error only when the consumer actually calls a method.\n nativeModule = null;\n}\n\nfunction getModule() {\n if (!nativeModule) {\n throw new Error(\n '[extended-vlc-player] Native module is not available. ' +\n 'Run `npx expo prebuild --clean` and rebuild the app, ' +\n 'or check that the plugin ran successfully.'\n );\n }\n return nativeModule;\n}\n\nfunction normalizeSource(source: ExtendedVlcSource) {\n if (typeof source === 'string') return { uri: source };\n const { uri } = source;\n if (!uri) {\n throw new Error('[extended-vlc-player] source.uri is required');\n }\n return { uri, headers: source.headers ?? null, drm: source.drm ?? null };\n}\n\nexport interface UseExtendedVlcPlayerOptions {\n /** Called once after the player is constructed. Useful for attaching PiP handlers. */\n onReady?: (player: ExtendedVlcPlayer) => void;\n}\n\n/**\n * Returns a stable player object for the given source. The native player\n * is created once (lazy on first method call) and replaced in place when\n * `source` changes via the same `replace` API used by `expo-video`.\n */\nexport function useExtendedVlcPlayer(\n source: ExtendedVlcSource,\n options: UseExtendedVlcPlayerOptions = {}\n): ExtendedVlcPlayer {\n const normalized = useMemo(() => normalizeSource(source), [source]);\n // We hold a numeric id of the \"current native source\" so we can detect\n // when the JS source prop has changed and tell the native module to swap.\n const [nativeId, setNativeId] = useState(0);\n const isMountedRef = useRef(true);\n\n // Event subscription is wired through the Fabric view component, not the\n // module — events flow back into the JS player object via the\n // ExtendedVlcPlayerView's callbacks. We keep the module call surface\n // (play/pause/seek/PiP) here.\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n // When the source prop changes, ask the native module to swap in place\n // instead of remounting the view (which would reset audio session, PiP\n // delegate wiring, etc.).\n try {\n getModule().replace(normalized);\n } catch (error) {\n // Surface as console error; the player view will also emit onError\n // once the Fabric event dispatcher picks it up.\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] replace failed:', error);\n }\n }, [normalized]);\n\n // The player object is intentionally stable across renders. Methods are\n // closures over `nativeId` so the latest normalized source is always\n // referenced, but the object identity does not change — consumers can\n // safely put it in dependency arrays.\n const player = useMemo<ExtendedVlcPlayer>(() => {\n const p: ExtendedVlcPlayer & { _nativeId: number } = {\n _nativeId: nativeId,\n play: () => getModule().play(nativeId),\n pause: () => getModule().pause(nativeId),\n stop: () => getModule().stop(nativeId),\n seek: (seconds: number) => getModule().seek(nativeId, seconds),\n setRate: (rate: number) => getModule().setRate(nativeId, rate),\n setVolume: (volume: number) => getModule().setVolume(nativeId, volume),\n setAudioTrack: (index: number) => getModule().setAudioTrack(nativeId, index),\n setSubtitleTrack: (index: number) => getModule().setSubtitleTrack(nativeId, index),\n replace: (next: ExtendedVlcSource) => {\n const nextNormalized = normalizeSource(next);\n try {\n getModule().replace({\n ...nextNormalized,\n // Tag the replace with the current nativeId so the existing\n // instance is updated rather than a new one allocated.\n instanceId: nativeId,\n });\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] replace failed:', error);\n }\n },\n startPictureInPicture: () => getModule().startPictureInPicture(nativeId),\n stopPictureInPicture: () => getModule().stopPictureInPicture(nativeId),\n isPictureInPictureActive: () => getModule().isPictureInPictureActive(nativeId),\n isPictureInPictureSupported: () => getModule().isPictureInPictureSupported(),\n };\n return p;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [nativeId]);\n\n // Bridge the native callback API. The hook surfaces the player as soon\n // as it exists, so consumers can attach PiP handlers from `onReady`.\n useEffect(() => {\n if (options.onReady) {\n try {\n options.onReady(player);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] onReady handler threw:', error);\n }\n }\n }, [player, options]);\n\n // Bump the native id once on mount so the first source prop is committed\n // to the native side. We use a ref-based \"first\" instead of an effect to\n // avoid an extra render.\n const firstRef = useRef(true);\n useEffect(() => {\n if (firstRef.current) {\n firstRef.current = false;\n setNativeId(1);\n }\n }, []);\n\n // Mark unused variables to keep TypeScript happy with strict checks.\n void useCallback;\n void isMountedRef;\n void (undefined as ExtendedVlcLoadEvent | ExtendedVlcProgressEvent | undefined);\n\n return player;\n}\n"]}
1
+ {"version":3,"file":"useExtendedVlcPlayer.js","sourceRoot":"","sources":["../src/useExtendedVlcPlayer.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { requireNativeModule } from 'expo-modules-core';\n\nimport type { ExtendedVlcPlayer, ExtendedVlcSource } from './types';\n\n// Native module is registered by the platform-specific module under the\n// JS module name \"ExtendedVlcPlayer\". The requireNativeModule helper resolves\n// it at runtime; if the native side is missing, accessing the module throws\n// — we surface a clear error so the consumer knows what failed instead of\n// silently producing a no-op player.\nlet nativeModule: any = null;\ntry {\n nativeModule = requireNativeModule('ExtendedVlcPlayer');\n} catch (error) {\n // Allow JS-side import of the module to succeed even when the native side\n // is not yet built (e.g. in a Jest test environment). We throw a helpful\n // error only when the consumer actually calls a method.\n nativeModule = null;\n}\n\nfunction getModule() {\n if (!nativeModule) {\n throw new Error(\n '[extended-vlc-player] Native module is not available. ' +\n 'Run `npx expo prebuild --clean` and rebuild the app, ' +\n 'or check that the plugin ran successfully.'\n );\n }\n return nativeModule;\n}\n\nfunction normalizeSource(source: ExtendedVlcSource) {\n if (typeof source === 'string') return { uri: source };\n const { uri } = source;\n if (!uri) {\n throw new Error('[extended-vlc-player] source.uri is required');\n }\n return { uri, headers: source.headers ?? null, drm: source.drm ?? null };\n}\n\nexport interface UseExtendedVlcPlayerOptions {\n /** Called once after the player is constructed. Useful for attaching PiP handlers. */\n onReady?: (player: ExtendedVlcPlayer) => void;\n}\n\n/**\n * Returns a stable player object for the given source. The native player\n * is created once on mount and replaced in place when `source` changes via\n * the same `replace` API used by `expo-video`.\n */\nexport function useExtendedVlcPlayer(\n source: ExtendedVlcSource,\n options: UseExtendedVlcPlayerOptions = {}\n): ExtendedVlcPlayer {\n const normalized = useMemo(() => normalizeSource(source), [source]);\n // We hold a numeric id of the \"current native source\" so we can detect\n // when the JS source prop has changed and tell the native module to swap.\n const [nativeId, setNativeId] = useState(0);\n const isMountedRef = useRef(true);\n\n // Event subscription is wired through the Fabric view component, not the\n // module — events flow back into the JS player object via the\n // ExtendedVlcPlayerView's callbacks. We keep the module call surface\n // (play/pause/seek/PiP) here.\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n let createdId = 0;\n try {\n createdId = getModule().createPlayer();\n setNativeId(createdId);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] player creation failed:', error);\n }\n\n return () => {\n if (createdId <= 0) return;\n try {\n getModule().destroyPlayer(createdId);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] player cleanup failed:', error);\n }\n };\n }, []);\n\n useEffect(() => {\n // When the source prop changes, ask the native module to swap in place\n // instead of remounting the view (which would reset audio session, PiP\n // delegate wiring, etc.).\n if (nativeId <= 0) return;\n try {\n getModule().replace(nativeId, normalized);\n } catch (error) {\n // Surface as console error; the player view will also emit onError\n // once the Fabric event dispatcher picks it up.\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] replace failed:', error);\n }\n }, [nativeId, normalized]);\n\n // The player object is intentionally stable across renders. Methods are\n // closures over `nativeId` so the latest normalized source is always\n // referenced, but the object identity does not change — consumers can\n // safely put it in dependency arrays.\n const player = useMemo<ExtendedVlcPlayer>(() => {\n const p: ExtendedVlcPlayer & { _nativeId: number } = {\n _nativeId: nativeId,\n play: () => getModule().play(nativeId),\n pause: () => getModule().pause(nativeId),\n stop: () => getModule().stop(nativeId),\n seek: (seconds: number) => getModule().seek(nativeId, seconds),\n setRate: (rate: number) => getModule().setRate(nativeId, rate),\n setVolume: (volume: number) => getModule().setVolume(nativeId, volume),\n setAudioTrack: (index: number) => getModule().setAudioTrack(nativeId, index),\n setSubtitleTrack: (index: number) => getModule().setSubtitleTrack(nativeId, index),\n replace: (next: ExtendedVlcSource) => {\n const nextNormalized = normalizeSource(next);\n try {\n getModule().replace(nativeId, nextNormalized);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] replace failed:', error);\n }\n },\n startPictureInPicture: () => getModule().startPictureInPicture(nativeId),\n stopPictureInPicture: () => getModule().stopPictureInPicture(nativeId),\n isPictureInPictureActive: () => getModule().isPictureInPictureActive(nativeId),\n isPictureInPictureSupported: () => getModule().isPictureInPictureSupported(),\n };\n return p;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [nativeId]);\n\n // Bridge the native callback API. The hook surfaces the player as soon\n // as it exists, so consumers can attach PiP handlers from `onReady`.\n useEffect(() => {\n if (options.onReady) {\n try {\n options.onReady(player);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] onReady handler threw:', error);\n }\n }\n }, [player, options]);\n\n // Keep the ref in the hook for consumers that unmount while a native call\n // is in flight; the native registry owns the actual session lifetime.\n void isMountedRef;\n\n return player;\n}\n"]}
@@ -19,6 +19,14 @@ public final class ExtendedVlcPlayerModule: Module {
19
19
  AudioSessionConfigurator.configureForPlayback()
20
20
  }
21
21
 
22
+ Function("createPlayer") { () -> Int in
23
+ PlayerRegistry.shared.next().0
24
+ }
25
+
26
+ Function("destroyPlayer") { (instanceId: Int) in
27
+ PlayerRegistry.shared.remove(id: instanceId)
28
+ }
29
+
22
30
  AsyncFunction("isPictureInPictureSupported") { () -> Bool in
23
31
  AVPictureInPictureController.isPictureInPictureSupported()
24
32
  }
@@ -33,6 +33,19 @@ RCT_EXPORT_VIEW_PROPERTY(onBuffering, RCTBubblingEventBlock)
33
33
  RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStart, RCTBubblingEventBlock)
34
34
  RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStop, RCTBubblingEventBlock)
35
35
 
36
+ - (void)setPlayer:(NSNumber *)player
37
+ {
38
+ NSNumber *nextId = player ?: @0;
39
+ if (self.playerId != nil && [self.playerId isEqualToNumber:nextId]) {
40
+ [self attachSessionIfPossible];
41
+ return;
42
+ }
43
+
44
+ [self detachSession];
45
+ self.playerId = nextId;
46
+ [self attachSessionIfPossible];
47
+ }
48
+
36
49
  + (ComponentDescriptorProvider)componentDescriptorProvider
37
50
  {
38
51
  return [RCTViewComponentView new];
@@ -49,23 +62,47 @@ RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStop, RCTBubblingEventBlock)
49
62
  - (void)didMoveToWindow
50
63
  {
51
64
  [super didMoveToWindow];
52
- if (self.window != nil && self.playerId == nil) {
53
- NSNumber *newId = [EXVLCPlayerRegistryBridge createSession];
54
- self.playerId = newId;
55
- NSValue *boxed = [EXVLCPlayerRegistryBridge sessionForId:newId];
56
- PlayerSession *session = (PlayerSession *)[boxed nonretainedObjectValue];
57
- if (session != nil) {
58
- session.drawable.frame = self.contentView.bounds;
59
- session.drawable.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
60
- [self.contentView addSubview:session.drawable];
61
- [EXVLCPlayerRegistryBridge attachEventSinks:newId view:self];
62
- }
65
+ [self attachSessionIfPossible];
66
+ }
67
+
68
+ - (void)attachSessionIfPossible
69
+ {
70
+ if (self.window == nil || self.playerId == nil || self.playerId.intValue == 0) {
71
+ return;
63
72
  }
73
+
74
+ NSValue *boxed = [EXVLCPlayerRegistryBridge sessionForId:self.playerId];
75
+ PlayerSession *session = (PlayerSession *)[boxed nonretainedObjectValue];
76
+ if (session == nil) {
77
+ return;
78
+ }
79
+
80
+ if (session.drawable.superview != self.contentView) {
81
+ session.drawable.frame = self.contentView.bounds;
82
+ session.drawable.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
83
+ [self.contentView addSubview:session.drawable];
84
+ }
85
+ [EXVLCPlayerRegistryBridge attachEventSinks:self.playerId view:self];
86
+ }
87
+
88
+ - (void)detachSession
89
+ {
90
+ if (self.playerId == nil || self.playerId.intValue == 0) {
91
+ return;
92
+ }
93
+
94
+ NSValue *boxed = [EXVLCPlayerRegistryBridge sessionForId:self.playerId];
95
+ PlayerSession *session = (PlayerSession *)[boxed nonretainedObjectValue];
96
+ if (session != nil && session.drawable.superview == self.contentView) {
97
+ [session.drawable removeFromSuperview];
98
+ }
99
+ [EXVLCPlayerRegistryBridge detachEventSinks:self.playerId];
64
100
  }
65
101
 
66
102
  - (void)dealloc
67
103
  {
68
- if (self.playerId != nil) {
104
+ [self detachSession];
105
+ if (self.playerId != nil && self.playerId.intValue != 0) {
69
106
  [EXVLCPlayerRegistryBridge destroySession:self.playerId];
70
107
  }
71
108
  }
@@ -56,6 +56,19 @@ final class PlayerRegistryBridge: NSObject {
56
56
  (view as? ExtendedVlcPlayerViewEventReceiver)?.exvlcEmit("onPictureInPictureStop", [:])
57
57
  }
58
58
  }
59
+
60
+ @objc static func detachEventSinks(_ id: NSNumber) {
61
+ guard let session = PlayerRegistry.shared.session(for: id.intValue) else { return }
62
+ session.onLoad = nil
63
+ session.onProgress = nil
64
+ session.onPlaying = nil
65
+ session.onPaused = nil
66
+ session.onEnded = nil
67
+ session.onError = nil
68
+ session.onBuffering = nil
69
+ session.onPictureInPictureStart = nil
70
+ session.onPictureInPictureStop = nil
71
+ }
59
72
  }
60
73
 
61
74
  /// Implemented by the Fabric component view (in Obj-C++) so that the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extended-vlc-player",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "React Native video player built on MobileVLCKit (iOS) and libVLC (Android) with true iOS Picture-in-Picture via AVSampleBufferDisplayLayer bridge.",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",
@@ -1,12 +1,7 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { requireNativeModule } from 'expo-modules-core';
3
3
 
4
- import type {
5
- ExtendedVlcLoadEvent,
6
- ExtendedVlcPlayer,
7
- ExtendedVlcProgressEvent,
8
- ExtendedVlcSource,
9
- } from './types';
4
+ import type { ExtendedVlcPlayer, ExtendedVlcSource } from './types';
10
5
 
11
6
  // Native module is registered by the platform-specific module under the
12
7
  // JS module name "ExtendedVlcPlayer". The requireNativeModule helper resolves
@@ -50,8 +45,8 @@ export interface UseExtendedVlcPlayerOptions {
50
45
 
51
46
  /**
52
47
  * Returns a stable player object for the given source. The native player
53
- * is created once (lazy on first method call) and replaced in place when
54
- * `source` changes via the same `replace` API used by `expo-video`.
48
+ * is created once on mount and replaced in place when `source` changes via
49
+ * the same `replace` API used by `expo-video`.
55
50
  */
56
51
  export function useExtendedVlcPlayer(
57
52
  source: ExtendedVlcSource,
@@ -75,19 +70,41 @@ export function useExtendedVlcPlayer(
75
70
  };
76
71
  }, []);
77
72
 
73
+ useEffect(() => {
74
+ let createdId = 0;
75
+ try {
76
+ createdId = getModule().createPlayer();
77
+ setNativeId(createdId);
78
+ } catch (error) {
79
+ // eslint-disable-next-line no-console
80
+ console.warn('[extended-vlc-player] player creation failed:', error);
81
+ }
82
+
83
+ return () => {
84
+ if (createdId <= 0) return;
85
+ try {
86
+ getModule().destroyPlayer(createdId);
87
+ } catch (error) {
88
+ // eslint-disable-next-line no-console
89
+ console.warn('[extended-vlc-player] player cleanup failed:', error);
90
+ }
91
+ };
92
+ }, []);
93
+
78
94
  useEffect(() => {
79
95
  // When the source prop changes, ask the native module to swap in place
80
96
  // instead of remounting the view (which would reset audio session, PiP
81
97
  // delegate wiring, etc.).
98
+ if (nativeId <= 0) return;
82
99
  try {
83
- getModule().replace(normalized);
100
+ getModule().replace(nativeId, normalized);
84
101
  } catch (error) {
85
102
  // Surface as console error; the player view will also emit onError
86
103
  // once the Fabric event dispatcher picks it up.
87
104
  // eslint-disable-next-line no-console
88
105
  console.warn('[extended-vlc-player] replace failed:', error);
89
106
  }
90
- }, [normalized]);
107
+ }, [nativeId, normalized]);
91
108
 
92
109
  // The player object is intentionally stable across renders. Methods are
93
110
  // closures over `nativeId` so the latest normalized source is always
@@ -107,12 +124,7 @@ export function useExtendedVlcPlayer(
107
124
  replace: (next: ExtendedVlcSource) => {
108
125
  const nextNormalized = normalizeSource(next);
109
126
  try {
110
- getModule().replace({
111
- ...nextNormalized,
112
- // Tag the replace with the current nativeId so the existing
113
- // instance is updated rather than a new one allocated.
114
- instanceId: nativeId,
115
- });
127
+ getModule().replace(nativeId, nextNormalized);
116
128
  } catch (error) {
117
129
  // eslint-disable-next-line no-console
118
130
  console.warn('[extended-vlc-player] replace failed:', error);
@@ -140,21 +152,9 @@ export function useExtendedVlcPlayer(
140
152
  }
141
153
  }, [player, options]);
142
154
 
143
- // Bump the native id once on mount so the first source prop is committed
144
- // to the native side. We use a ref-based "first" instead of an effect to
145
- // avoid an extra render.
146
- const firstRef = useRef(true);
147
- useEffect(() => {
148
- if (firstRef.current) {
149
- firstRef.current = false;
150
- setNativeId(1);
151
- }
152
- }, []);
153
-
154
- // Mark unused variables to keep TypeScript happy with strict checks.
155
- void useCallback;
155
+ // Keep the ref in the hook for consumers that unmount while a native call
156
+ // is in flight; the native registry owns the actual session lifetime.
156
157
  void isMountedRef;
157
- void (undefined as ExtendedVlcLoadEvent | ExtendedVlcProgressEvent | undefined);
158
158
 
159
159
  return player;
160
160
  }