Skip to content

Commit 1767bb1

Browse files
committed
Fix: Youtube video blur on isBlur
1 parent 090d487 commit 1767bb1

5 files changed

Lines changed: 81 additions & 35 deletions

File tree

android/src/main/java/com/video/PlayerCore.kt

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import android.webkit.RenderProcessGoneDetail
1919
import android.webkit.WebView
2020
import android.webkit.WebViewClient
2121
import android.widget.FrameLayout
22+
import androidx.annotation.RequiresApi
2223
import androidx.media3.common.C
2324
import androidx.media3.common.MediaItem
2425
import androidx.media3.common.MediaMetadata
@@ -109,15 +110,15 @@ object PlayerCore {
109110
private var blurType = "dark"
110111

111112
/**
112-
* Real-time blur of the actual PlayerView, not an overlay above it — a
113-
* `RenderEffect` composited by the GPU directly onto the view that's
114-
* rendering the video, so it blurs a live TextureView the way a bitmap-
115-
* snapshot blur library can't.
113+
* Real-time blur of the actual video output, not an overlay above it — a
114+
* `RenderEffect` composited by the GPU directly onto whichever engine's
115+
* view is currently active (the PlayerView, or the YouTube engine's plain
116+
* WebView — both are ordinary Views a RenderEffect blurs the same way), so
117+
* it blurs a live TextureView the way a bitmap-snapshot blur library can't.
116118
*
117119
* API 31+ only: `RenderEffect` doesn't exist below Android 12, and there's
118120
* no reasonable fallback that would actually blur (vs. just covering) the
119-
* video, so this is a silent no-op there — same as it already is for the
120-
* WebView (YouTube) engine, which has no PlayerView to blur at all.
121+
* video, so this is a silent no-op there.
121122
*
122123
* `blurAmount` (0-100) maps to the blur radius; `blurType` tints the blur
123124
* (`"dark"` a black tint, `"light"`/`"xlight"` white at decreasing
@@ -132,17 +133,29 @@ object PlayerCore {
132133
}
133134

134135
/**
135-
* Re-applied whenever the PlayerView is (re)created — [setBlurred] can be
136-
* called before the view exists yet (e.g. before the first attach), and
137-
* the desired state must survive that.
136+
* Re-applied whenever the PlayerView/WebView is (re)created, and whenever
137+
* the active engine changes (a `RenderEffect` lives on one specific View
138+
* instance, so switching from url <-> youtube must move it) — see the
139+
* calls to this from [ensurePlayerView], [ensureWebView] and [attachTo].
140+
* [setBlurred] can also be called before either view exists yet (e.g.
141+
* before the first attach), and the desired state must survive that.
138142
*/
139143
private fun applyBlur() {
140144
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
141-
val view = playerView ?: return
142-
if (!blurred) {
143-
view.setRenderEffect(null)
144-
return
145+
val effect = if (blurred) buildBlurEffect() else null
146+
// Always clear the inactive engine's view too, or a stale blur would
147+
// reappear with no explicit setBlurred call if the engine switches back.
148+
if (engine == Engine.WEB) {
149+
webView?.setRenderEffect(effect)
150+
playerView?.setRenderEffect(null)
151+
} else {
152+
playerView?.setRenderEffect(effect)
153+
webView?.setRenderEffect(null)
145154
}
155+
}
156+
157+
@RequiresApi(Build.VERSION_CODES.S)
158+
private fun buildBlurEffect(): RenderEffect {
146159
// Radius 0 renders as a no-op blur but a jarring instant tint pop-in, so
147160
// floor it slightly once "blurred" is true at all.
148161
val radius = (blurAmount.coerceIn(0.0, 100.0) / 100.0 * 25.0).coerceAtLeast(1.0).toFloat()
@@ -152,11 +165,10 @@ object PlayerCore {
152165
"xlight" -> Color.argb(60, 255, 255, 255)
153166
else -> Color.argb(120, 0, 0, 0) // "dark"
154167
}
155-
val tinted = RenderEffect.createColorFilterEffect(
168+
return RenderEffect.createColorFilterEffect(
156169
PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_OVER),
157170
blur
158171
)
159-
view.setRenderEffect(tinted)
160172
}
161173

162174
// Second engine: a re-parentable WebView running the YouTube IFrame API.
@@ -425,6 +437,7 @@ object PlayerCore {
425437
}
426438
}
427439
webView = wv
440+
applyBlur()
428441
return wv
429442
}
430443

@@ -961,6 +974,9 @@ setInterval(function(){if(player&&player.getCurrentTime){post({type:'time',posit
961974
if (engine == Engine.EXO) {
962975
playerView?.let { rebindPlayerToView(it, surfaceId, container) }
963976
}
977+
// Keeps the blur (if any) on whichever engine's view is now active —
978+
// `view` above may be a different engine's view than last time.
979+
applyBlur()
964980
listener?.onAttach(surfaceId)
965981
}
966982

ios/VideoPlayerCore.swift

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,11 @@ public final class VideoPlayerCore: NSObject {
7474
private let hostView = VideoHostView()
7575

7676
// --- blur (real-time, over the actual video output — see setBlurred) ---
77-
// A real UIVisualEffectView layered over the host view. Unlike a bitmap-
78-
// snapshot blur, this composites at the system level and reliably blurs
79-
// the live AVPlayerLayer content underneath, on every iOS version.
77+
// A real UIVisualEffectView, moved onto whichever engine view is active
78+
// (see attachBlurViewIfNeeded). Unlike a bitmap-snapshot blur, this
79+
// composites at the system level and reliably blurs the live content
80+
// underneath — the native player's AVPlayerLayer, or the YouTube engine's
81+
// WKWebView — on every iOS version.
8082
private lazy var blurView: UIVisualEffectView = {
8183
let view = UIVisualEffectView(effect: nil)
8284
view.isUserInteractionEnabled = false
@@ -86,6 +88,9 @@ public final class VideoPlayerCore: NSObject {
8688
/// Retained so the effect it applies via `fractionComplete` (below) isn't
8789
/// reverted by deallocation.
8890
private var blurAnimator: UIViewPropertyAnimator?
91+
/// Guards `attachBlurViewIfNeeded` so it stays a no-op (no lazy `blurView`
92+
/// creation, no extra subview) until `setBlurred` has actually been used.
93+
private var blurEverUsed = false
8994

9095
// Second engine: a re-parentable WKWebView running the YouTube IFrame API.
9196
private enum Engine { case exo, web }
@@ -866,21 +871,35 @@ public final class VideoPlayerCore: NSObject {
866871
}
867872
}
868873

874+
/// Moves the blur subview onto whichever engine view is currently active
875+
/// (the native player's `hostView`, or the YouTube engine's `WKWebView`) —
876+
/// both are plain `UIView`s, so a `UIVisualEffectView` blurs either one
877+
/// the same way. Called from `setBlurred` and, so the blur keeps following
878+
/// the video across an engine switch (`url` \<-\> `youtube`) or a re-parent
879+
/// even without a fresh `setBlurred` call, from `attachTo` too.
880+
private func attachBlurViewIfNeeded() {
881+
guard blurEverUsed else { return }
882+
let parent = activeView
883+
guard blurView.superview !== parent else { return }
884+
blurView.removeFromSuperview()
885+
parent.addSubview(blurView)
886+
NSLayoutConstraint.activate([
887+
blurView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
888+
blurView.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
889+
blurView.topAnchor.constraint(equalTo: parent.topAnchor),
890+
blurView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
891+
])
892+
}
893+
869894
/// `amount` is 0-100 intensity, mapped onto the blur via the standard
870895
/// `UIViewPropertyAnimator.fractionComplete` trick (a real, partial-way-
871896
/// applied blur, not a faked opacity fade). `type` picks the system blur
872897
/// style — matching @react-native-community/blur's naming for familiarity,
873898
/// though this is this library's own implementation, not that package.
899+
/// Works for both engines — see `attachBlurViewIfNeeded`.
874900
@objc public func setBlurred(_ blurred: Bool, amount: Double, type: String) {
875-
if blurView.superview !== hostView {
876-
hostView.addSubview(blurView)
877-
NSLayoutConstraint.activate([
878-
blurView.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
879-
blurView.trailingAnchor.constraint(equalTo: hostView.trailingAnchor),
880-
blurView.topAnchor.constraint(equalTo: hostView.topAnchor),
881-
blurView.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
882-
])
883-
}
901+
blurEverUsed = true
902+
attachBlurViewIfNeeded()
884903
blurAnimator?.stopAnimation(true)
885904
blurAnimator = nil
886905
guard blurred else {
@@ -965,6 +984,9 @@ public final class VideoPlayerCore: NSObject {
965984
view.frame = container.bounds
966985
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
967986
container.addSubview(view)
987+
// Keeps the blur (if any) following the video across an engine switch —
988+
// `view` above may now be a different engine's view than last time.
989+
attachBlurViewIfNeeded()
968990
currentSurfaceId = surfaceId
969991
pendingSurfaceId = nil
970992
delegate?.onAttach(surfaceId)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-video-provider",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "Singleton-engine video library for React Native (one native player, many surfaces)",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

src/NativeVideo.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,12 @@ export interface Spec extends TurboModule {
135135
* `blurAmount` is 0-100 intensity; `blurType` is `"dark"` | `"light"` |
136136
* `"xlight"` (naming matches @react-native-community/blur for familiarity).
137137
*
138-
* Android: a `RenderEffect` composited onto the PlayerView — API 31+ only
139-
* (silent no-op below that, and for the YouTube/WebView engine, which has
140-
* no video output view to blur). iOS: a `UIVisualEffectView` layered over
141-
* the video output, supported on every version.
138+
* Works for both engines — the native player AND YouTube (WebView) — and
139+
* keeps following the video across an engine switch or a surface re-parent.
140+
*
141+
* Android: a `RenderEffect` composited onto whichever engine's view is
142+
* currently active — API 31+ only (silent no-op below that). iOS: a
143+
* `UIVisualEffectView` layered the same way, supported on every version.
142144
*/
143145
setBlurred(blurred: boolean, blurAmount: number, blurType: string): void;
144146

src/components/VideoPlayer.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,15 @@ export interface VideoPlayerProps extends ViewProps {
147147
* directly onto the video output, not an overlay drawn above it — so it
148148
* blurs the actual picture, not just whatever's on top of it.
149149
*
150-
* Android needs `useTextureView` (which this library defaults to `true`)
151-
* AND API 31+ (Android 12) — below that, `isBlur` is a silent no-op and
152-
* the video stays unblurred. iOS supports every version.
150+
* Works for both a native `url` source and YouTube alike, and keeps
151+
* following the video across an engine switch, fullscreen, or floating.
152+
*
153+
* Android needs API 31+ (Android 12) — below that, `isBlur` is a silent
154+
* no-op and the video stays unblurred. A native (non-YouTube) source also
155+
* needs `useTextureView` (which this library defaults to `true`) — a
156+
* SurfaceView renders on a separate hardware layer the blur can't reach,
157+
* so with `useTextureView: false` the video shows through unblurred. iOS
158+
* supports every version, for both engines.
153159
*/
154160
isBlur?: boolean;
155161
/** Blur tint. Default `'dark'` (black). */

0 commit comments

Comments
 (0)