Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions media_kit_video/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,99 @@

Native implementation for video playback in [package:media_kit](https://pub.dev/packages/media_kit).

## Native rendering-mode integration (Darwin)

The registered plugin publishes its `MediaKitVideoPlugin` instance through
Flutter's plugin registry. Native integrations can use this typed API without
intercepting the plugin's method channel or replaying its create/dispose messages.
Call these APIs and consume their callbacks on the Flutter platform/main thread.

```swift
import media_kit_video

let video = registry.valuePublished(byPlugin: "MediaKitVideoPlugin")
as? MediaKitVideoPlugin

// Also keep outputs created by playlist/episode changes on the software path.
video?.setSoftwareRenderingForNewOutputs(true)

// Suspend this player's video track (vid=no) and confirm it before calling.
video?.setSoftwareRendering(handle: handle, enabled: true) { result in
switch result {
case .changed:
// Replacement context exists and its new texture ID has been emitted.
// Restore the saved vid after the Dart texture listener observes it.
break
case .unchanged:
// Already in this mode, or hardware rendering was disabled originally.
// Restore vid directly; no new texture ID will be emitted.
break
case .notReady:
// Another initialization/transition is pending. Restore vid directly,
// then retry after that output becomes ready; do not wait for a new ID
// inside this request.
break
case .failed:
// Restore vid directly as well. Do not leave the player disabled.
break
}
}
```

`enabled: false` restores the original output configuration, including the user's
hardware-rendering preference. It does not force GPU rendering for an originally
software output. The temporary override does not replace the mpv player or alter
its media, audio, playback position, decoder configuration, or shader options.
Software rendering does not apply GPU shader effects. A compatible copy-back
hardware decoder is needed when switching from GPU to software rendering; this
API deliberately does not change the player's decoder settings.

The caller owns the video-track suspension/restoration transaction and must
serialize it per player, retain the exact player instance, and restore `vid` in
all completion paths. The plugin owns output configuration, the latest dimensions,
context/texture replacement, and disposal. Completion waits for initialization
and texture registration, not the first frame: a frame cannot be produced until
`vid` is restored. Overlapping initialization/transitions return `.notReady`;
unknown/disposed handles and outputs whose recovery failed return `.failed`.
Only `.notReady` permits a readiness-triggered retry. Retrying `.failed` on every
texture registration would loop when hardware is unavailable or the simulator
falls back to software. A target initialization failure attempts to recreate the
previous renderer before reporting `.failed`. If that recovery also fails, the
output is not reported as ready; the integration must surface a playback failure
or recreate the player. Disposal remains available and drains all resources.

On iOS, request GPU restoration only when the application is active. The plugin
also checks application state before replacing an output, and guards the state
check plus GPU-context initialization in one platform-thread turn. Existing GPU
outputs gate resize/render work when the application resigns active; the lifecycle
callback waits for an in-flight GL section, and activation requests a fresh frame.
Property reads and platform notifications stay outside that render lock. If the app backgrounds during restoration, it keeps/recreates a
software output and reports `.failed`; the handle remains in
`softwareRenderingHandles` so restoration can be retried on the next activation.
Do not interpret `.failed` as proof that the old texture ID still exists.

When leaving background playback, set the new-output default back to `false`,
then iterate `softwareRenderingHandles` and run the same suspend / set / restore
transaction with `enabled: false`. Changing the default alone does not replace
existing outputs. If a handle is still being initialized, retry after its normal
video-controller readiness signal. While background restoration is pending,
keep the new-output default enabled so later playlist/episode outputs stay safe.

Run the deterministic native lifecycle tests with:

```sh
media_kit_video/tool/rendering_mode_test/run.sh
```

These compile the real manager against controlled output doubles and cover
readiness, resize races, duplicate requests, initialization failures/rollback,
disposal, handle reuse, and backgrounding during GPU restoration. Additional
checks use the real output worker to verify delayed context readiness, safe
queued disposal, initialization failure, and the concurrent background render gate. They do not
replace device validation: the iOS simulator forces software rendering, so a
physical device is required to validate GPU -> software -> GPU, PiP background
frames, and shader restoration.

## License

Copyright © 2021 & onwards, Hitesh Kumar Saini <<saini123hitesh@gmail.com>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
utils: utils
)
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.publish(instance)
}

private let channel: FlutterMethodChannel
Expand All @@ -49,6 +50,30 @@ public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
self.utils = utils
}

/// Native renderer API for integrations such as iOS sample-buffer PiP.
/// Must run on the platform thread. Suspend the player's video track before
/// calling and restore it after completion, even when the request fails.
/// `changed` means the replacement context is initialized and its Flutter
/// texture is registered; it does not wait for a frame while vid is disabled.
public func setSoftwareRendering(
handle: Int64,
enabled: Bool,
completion: @escaping (VideoOutputRenderingResult) -> Void
) {
videoOutputManager.setSoftwareRendering(handle: handle, enabled: enabled, completion: completion)
}

/// Override the renderer of future outputs without changing the original
/// configuration. Existing outputs still require setSoftwareRendering.
public func setSoftwareRenderingForNewOutputs(_ enabled: Bool) {
videoOutputManager.setSoftwareRenderingForNewOutputs(enabled)
}

/// Handles that need their original GPU preference restored after PiP.
public var softwareRenderingHandles: [Int64] {
videoOutputManager.softwareRenderingHandles
}

public func handle(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
Expand All @@ -71,7 +96,7 @@ public class MediaKitVideoPlugin: NSObject, FlutterPlugin {

private func handleCreateMethodCall(
_ arguments: Any?,
_ result: FlutterResult
_ result: @escaping FlutterResult
) {
let args = arguments as? [String: Any]
guard let library = args?["libmpv"] as? String,
Expand Down Expand Up @@ -104,10 +129,15 @@ public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
],
] as [String: Any]
)
},
completion: { ready in
if ready {
result(nil)
} else {
result(FlutterError(code: "video_output", message: "Could not initialize the video output.", details: nil))
}
}
)

result(nil)
}

private func handleSetSizeMethodCall(
Expand Down
19 changes: 11 additions & 8 deletions media_kit_video/common/darwin/Classes/plugin/TextureSW.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
skipCheckArgs: true
)

init(
init?(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
Expand All @@ -32,9 +32,9 @@ public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {

super.init()

DispatchQueue.main.async {
self.initMPV()
}
// Construct on the output worker so readiness means the context exists,
// and disposal can never overtake a deferred main-thread initialization.
guard self.initMPV() else { return nil }
}

deinit {
Expand All @@ -55,7 +55,7 @@ public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}

private func initMPV() {
private func initMPV() -> Bool {
let api = UnsafeMutableRawPointer(
mutating: (MPV_RENDER_API_TYPE_SW as NSString).utf8String
)
Expand All @@ -64,9 +64,11 @@ public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]

MPVHelpers.checkError(
media_kit_mpv_render_context_create(&renderContext, handle, &params)
)
let status = media_kit_mpv_render_context_create(&renderContext, handle, &params)
guard status >= 0, renderContext != nil else {
NSLog("TextureSW: render context initialization failed (%d)", status)
return false
}

media_kit_mpv_render_context_set_update_callback(
renderContext,
Expand All @@ -78,6 +80,7 @@ public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
},
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
return true
}

private func disposeMPV() {
Expand Down
Loading