forked from Foxyz/sync_replay_project
Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)
Flutter >= 3.38 keeps its EGL contexts on the raster thread, so media_kit's video output found none on the platform thread and fell back to software rendering (no shaders, ~5 fps) on X11. The patched output opens Flutter's EGL display itself, steps GDK's GLX context aside on X11 and releases its own context. Patch from a friend's agent, kept in third_party/media_kit_video/starsync/. Wayland/NVIDIA still renders on the GPU (checked); the X11 path was verified on the friend's machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25b19f2d6a
commit
a87ee98faa
131 files changed
+15532
-6
No files matched your search
@@ -0,0 +1,30 @@
|
||||
public enum MPVHelpers {
|
||||
public static func checkError(_ status: CInt) {
|
||||
if status < 0 {
|
||||
NSLog("MPVHelpers: error: \(String(cString: mpv_error_string(status)))")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
public static func getVideoOutParams(
|
||||
_ handle: OpaquePointer
|
||||
) -> MPVVideoOutParams {
|
||||
var node = mpv_node()
|
||||
defer {
|
||||
mpv_free_node_contents(&node)
|
||||
}
|
||||
|
||||
mpv_get_property(handle, "video-out-params", MPV_FORMAT_NODE, &node)
|
||||
|
||||
if node.format != MPV_FORMAT_NODE_MAP {
|
||||
return MPVVideoOutParams.empty
|
||||
}
|
||||
|
||||
let map: mpv_node_list = node.u.list!.pointee
|
||||
if map.num == 0 {
|
||||
return MPVVideoOutParams.empty
|
||||
}
|
||||
|
||||
return MPVVideoOutParams.fromMPVNodeList(map)
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
public class MPVVideoOutParams {
|
||||
public let dw: Int64
|
||||
public let dh: Int64
|
||||
public let rotate: Int64
|
||||
|
||||
init(
|
||||
dw: Int64,
|
||||
dh: Int64,
|
||||
rotate: Int64
|
||||
) {
|
||||
self.dw = dw
|
||||
self.dh = dh
|
||||
self.rotate = rotate
|
||||
}
|
||||
|
||||
public static let empty = MPVVideoOutParams(
|
||||
dw: 0,
|
||||
dh: 0,
|
||||
rotate: 0
|
||||
)
|
||||
|
||||
public static func fromMPVNodeList(
|
||||
_ map: mpv_node_list
|
||||
) -> MPVVideoOutParams {
|
||||
var dw: Int64 = 0
|
||||
var dh: Int64 = 0
|
||||
var rotate: Int64 = 0
|
||||
|
||||
var kptr = map.keys!
|
||||
var vptr = map.values!
|
||||
for _ in 0 ..< map.num {
|
||||
let key = String(cString: kptr.pointee!)
|
||||
let value: mpv_node = vptr.pointee
|
||||
|
||||
kptr = kptr.successor()
|
||||
vptr = vptr.successor()
|
||||
|
||||
if value.format == MPV_FORMAT_INT64 {
|
||||
if key == "dw" {
|
||||
dw = value.u.int64
|
||||
} else if key == "dh" {
|
||||
dh = value.u.int64
|
||||
} else if key == "rotate" {
|
||||
rotate = value.u.int64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MPVVideoOutParams(
|
||||
dw: dw,
|
||||
dh: dh,
|
||||
rotate: rotate
|
||||
)
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
|
||||
private static let CHANNEL_NAME = "com.alexmercerind/media_kit_video"
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
#if canImport(Flutter)
|
||||
let binaryMessenger = registrar.messenger()
|
||||
let registry = registrar.textures()
|
||||
let utils: UtilsProtocol? = nil
|
||||
#elseif canImport(FlutterMacOS)
|
||||
let binaryMessenger = registrar.messenger
|
||||
let registry = registrar.textures
|
||||
let utils: UtilsProtocol? = Utils(registrar)
|
||||
#endif
|
||||
|
||||
let channel = FlutterMethodChannel(
|
||||
name: CHANNEL_NAME,
|
||||
binaryMessenger: binaryMessenger
|
||||
)
|
||||
let instance = MediaKitVideoPlugin(
|
||||
registry: registry,
|
||||
channel: channel,
|
||||
utils: utils
|
||||
)
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
private let channel: FlutterMethodChannel
|
||||
private let videoOutputManager: VideoOutputManager
|
||||
private let utils: UtilsProtocol?
|
||||
|
||||
init(
|
||||
registry: FlutterTextureRegistry,
|
||||
channel: FlutterMethodChannel,
|
||||
utils: UtilsProtocol?
|
||||
) {
|
||||
self.channel = channel
|
||||
videoOutputManager = VideoOutputManager(
|
||||
registry: registry
|
||||
)
|
||||
self.utils = utils
|
||||
}
|
||||
|
||||
public func handle(
|
||||
_ call: FlutterMethodCall,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
switch call.method {
|
||||
case "VideoOutputManager.Create":
|
||||
handleCreateMethodCall(call.arguments, result)
|
||||
case "VideoOutputManager.SetSize":
|
||||
handleSetSizeMethodCall(call.arguments, result)
|
||||
case "VideoOutputManager.Dispose":
|
||||
handleDisposeMethodCall(call.arguments, result)
|
||||
case "Utils.EnterNativeFullscreen":
|
||||
handleEnterNativeFullscreenMethodCall(call.arguments, result)
|
||||
case "Utils.ExitNativeFullscreen":
|
||||
handleExitNativeFullscreenMethodCall(call.arguments, result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCreateMethodCall(
|
||||
_ arguments: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
let args = arguments as? [String: Any]
|
||||
let handleStr = args?["handle"] as! String
|
||||
let handle: Int64? = Int64(handleStr)
|
||||
let configDict = args?["configuration"] as! [String: Any]
|
||||
let configuration = VideoOutputConfiguration.fromDict(configDict)
|
||||
|
||||
assert(handle != nil, "handle must be an Int64")
|
||||
|
||||
videoOutputManager.create(
|
||||
handle: handle!,
|
||||
configuration: configuration,
|
||||
textureUpdateCallback: { (_ textureId: Int64, _ size: CGSize) in
|
||||
self.channel.invokeMethod(
|
||||
"VideoOutput.Resize",
|
||||
arguments: [
|
||||
"handle": handle!,
|
||||
"id": textureId,
|
||||
"rect": [
|
||||
"top": 0,
|
||||
"left": 0,
|
||||
"width": size.width,
|
||||
"height": size.height,
|
||||
],
|
||||
] as [String: Any]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleSetSizeMethodCall(
|
||||
_ arguments: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
let args = arguments as? [String: Any]
|
||||
let handleStr = args?["handle"] as! String
|
||||
let widthStr = args?["width"] as! String
|
||||
let heightStr = args?["height"] as! String
|
||||
|
||||
let handle: Int64? = Int64(handleStr)
|
||||
let width: Int64? = Int64(widthStr)
|
||||
let height: Int64? = Int64(heightStr)
|
||||
|
||||
assert(handle != nil, "handle must be an Int64")
|
||||
|
||||
self.videoOutputManager.setSize(
|
||||
handle: handle!,
|
||||
width: width,
|
||||
height: height
|
||||
)
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleDisposeMethodCall(
|
||||
_ arguments: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
let args = arguments as? [String: Any]
|
||||
let handleStr = args?["handle"] as! String
|
||||
let handle: Int64? = Int64(handleStr)
|
||||
|
||||
assert(handle != nil, "handle must be an Int64")
|
||||
|
||||
videoOutputManager.destroy(
|
||||
handle: handle!
|
||||
)
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleEnterNativeFullscreenMethodCall(
|
||||
_: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
if utils == nil {
|
||||
return result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
utils?.enterNativeFullscreen()
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleExitNativeFullscreenMethodCall(
|
||||
_: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
if utils == nil {
|
||||
return result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
utils?.exitNativeFullscreen()
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public protocol ResizableTextureProtocol: NSObject, FlutterTexture {
|
||||
func resize(_ size: CGSize)
|
||||
func render(_ size: CGSize)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
// This class avoids data race when called from a thread
|
||||
public class SafeResizableTexture:
|
||||
NSObject,
|
||||
FlutterTexture,
|
||||
ResizableTextureProtocol
|
||||
{
|
||||
private let lock = NSRecursiveLock()
|
||||
private let child: ResizableTextureProtocol
|
||||
|
||||
init(_ child: ResizableTextureProtocol) {
|
||||
self.child = child
|
||||
}
|
||||
|
||||
public func resize(_ size: CGSize) {
|
||||
return locked {
|
||||
return child.resize(size)
|
||||
}
|
||||
}
|
||||
|
||||
public func render(_ size: CGSize) {
|
||||
return locked {
|
||||
return child.render(size)
|
||||
}
|
||||
}
|
||||
|
||||
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
return child.copyPixelBuffer()
|
||||
}
|
||||
|
||||
private func locked<T>(do block: () -> T) -> T {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return block()
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// This class was created to prevent a frameBuffer read by Flutter from being
|
||||
// concurrently modified by a write method (mpv's renderer).
|
||||
//
|
||||
// To do this, two pools are set up, one for frameBuffers available for writing,
|
||||
// the other for frameBuffers ready to be read by Flutter.
|
||||
//
|
||||
// When a frameBuffer has finished being modified and is "pushed", the oldest
|
||||
// ready frameBuffer is marked as the current frameBuffer, and the oldest
|
||||
// current frameBuffer is placed in the pool of frameBuffers available for
|
||||
// writing.
|
||||
//
|
||||
// The use of at least three frameBuffers ensures that the read and write phases
|
||||
// do not overlap, thus eliminating flicker.
|
||||
public class SwappableObjectManager<T> {
|
||||
private let lock: NSRecursiveLock = NSRecursiveLock()
|
||||
private var available: [T]
|
||||
private var ready: [T] = []
|
||||
private var _current: T?
|
||||
|
||||
init(objects: [T], skipCheckArgs: Bool = false) {
|
||||
if !skipCheckArgs {
|
||||
SwappableObjectManager.checkArgs(objects)
|
||||
}
|
||||
|
||||
available = objects
|
||||
}
|
||||
|
||||
public func reinit(objects: [T], skipCheckArgs: Bool = false) {
|
||||
if !skipCheckArgs {
|
||||
SwappableObjectManager.checkArgs(objects)
|
||||
}
|
||||
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
available = objects
|
||||
ready = []
|
||||
_current = nil
|
||||
}
|
||||
|
||||
public func nextAvailable() -> T? {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
let object: T? =
|
||||
available.count > 0
|
||||
? available.removeFirst()
|
||||
: nil
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
public func pushAsReady(_ object: T) {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
ready.append(object)
|
||||
updateCurrent()
|
||||
}
|
||||
|
||||
public var current: T? {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return _current
|
||||
}
|
||||
|
||||
private func updateCurrent() {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
let next: T? =
|
||||
ready.count > 0
|
||||
? ready.removeFirst()
|
||||
: nil
|
||||
|
||||
if next == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let old: T? = _current
|
||||
_current = next
|
||||
|
||||
if old == nil {
|
||||
return
|
||||
}
|
||||
|
||||
available.append(old!)
|
||||
}
|
||||
|
||||
static private func checkArgs(_ objects: [T]) {
|
||||
if objects.count < 2 {
|
||||
NSLog("SwappableObjectManager: require at least two objects to work")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
|
||||
public typealias UpdateCallback = () -> Void
|
||||
|
||||
private let handle: OpaquePointer
|
||||
private let updateCallback: UpdateCallback
|
||||
private var renderContext: OpaquePointer?
|
||||
private var textureContexts = SwappableObjectManager<TextureSWContext>(
|
||||
objects: [],
|
||||
skipCheckArgs: true
|
||||
)
|
||||
|
||||
init(
|
||||
handle: OpaquePointer,
|
||||
updateCallback: @escaping UpdateCallback
|
||||
) {
|
||||
self.handle = handle
|
||||
self.updateCallback = updateCallback
|
||||
|
||||
super.init()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.initMPV()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
disposePixelBuffer()
|
||||
disposeMPV()
|
||||
}
|
||||
|
||||
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
let textureContext = textureContexts.current
|
||||
if textureContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Unmanaged.passRetained(textureContext!.pixelBuffer)
|
||||
}
|
||||
|
||||
private func initMPV() {
|
||||
let api = UnsafeMutableRawPointer(
|
||||
mutating: (MPV_RENDER_API_TYPE_SW as NSString).utf8String
|
||||
)
|
||||
var params: [mpv_render_param] = [
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
|
||||
]
|
||||
|
||||
MPVHelpers.checkError(
|
||||
mpv_render_context_create(&renderContext, handle, ¶ms)
|
||||
)
|
||||
|
||||
mpv_render_context_set_update_callback(
|
||||
renderContext,
|
||||
{ (ctx) in
|
||||
let that = unsafeBitCast(ctx, to: TextureSW.self)
|
||||
DispatchQueue.main.async {
|
||||
that.updateCallback()
|
||||
}
|
||||
},
|
||||
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
)
|
||||
}
|
||||
|
||||
private func disposeMPV() {
|
||||
mpv_render_context_set_update_callback(renderContext, nil, nil)
|
||||
mpv_render_context_free(renderContext)
|
||||
}
|
||||
|
||||
public func resize(_ size: CGSize) {
|
||||
if size.width == 0 || size.height == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("TextureSW: resize: \(size.width)x\(size.height)")
|
||||
createPixelBuffer(size)
|
||||
}
|
||||
|
||||
private func createPixelBuffer(_ size: CGSize) {
|
||||
disposePixelBuffer()
|
||||
|
||||
textureContexts.reinit(
|
||||
objects: [
|
||||
TextureSWContext(size: size),
|
||||
TextureSWContext(size: size),
|
||||
TextureSWContext(size: size),
|
||||
],
|
||||
skipCheckArgs: true
|
||||
)
|
||||
}
|
||||
|
||||
private func disposePixelBuffer() {
|
||||
textureContexts.reinit(objects: [], skipCheckArgs: true)
|
||||
}
|
||||
|
||||
public func render(_ size: CGSize) {
|
||||
let textureContext = textureContexts.nextAvailable()
|
||||
if textureContext == nil {
|
||||
return
|
||||
}
|
||||
|
||||
CVPixelBufferLockBaseAddress(
|
||||
textureContext!.pixelBuffer,
|
||||
CVPixelBufferLockFlags(rawValue: 0)
|
||||
)
|
||||
defer {
|
||||
CVPixelBufferUnlockBaseAddress(
|
||||
textureContext!.pixelBuffer,
|
||||
CVPixelBufferLockFlags(rawValue: 0)
|
||||
)
|
||||
}
|
||||
|
||||
var ssize: [Int32] = [Int32(size.width), Int32(size.height)]
|
||||
let format: String = "bgr0"
|
||||
var pitch: Int = CVPixelBufferGetBytesPerRow(textureContext!.pixelBuffer)
|
||||
let buffer = CVPixelBufferGetBaseAddress(textureContext!.pixelBuffer)
|
||||
|
||||
// pointers
|
||||
let ssizePtr = ssize.withUnsafeMutableBytes {
|
||||
$0.baseAddress?.assumingMemoryBound(to: Int32.self)
|
||||
}
|
||||
let formatPtr = UnsafeMutablePointer(
|
||||
mutating: (format as NSString).utf8String
|
||||
)
|
||||
let pitchPtr = withUnsafeMutablePointer(to: &pitch) { $0 }
|
||||
let bufferPtr = buffer!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
var params: [mpv_render_param] = [
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_SW_SIZE, data: ssizePtr),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_SW_FORMAT, data: formatPtr),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_SW_STRIDE, data: pitchPtr),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_SW_POINTER, data: bufferPtr),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
|
||||
]
|
||||
|
||||
mpv_render_context_render(renderContext, ¶ms)
|
||||
|
||||
textureContexts.pushAsReady(textureContext!)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
public class TextureSWContext {
|
||||
public let pixelBuffer: CVPixelBuffer
|
||||
|
||||
init(size: CGSize) {
|
||||
self.pixelBuffer = TextureSWContext.createPixelBuffer(size)
|
||||
}
|
||||
|
||||
deinit {
|
||||
TextureSWContext.disposePixelBuffer(pixelBuffer)
|
||||
}
|
||||
|
||||
private static func createPixelBuffer(_ size: CGSize) -> CVPixelBuffer {
|
||||
let attrs =
|
||||
[
|
||||
kCVPixelBufferMetalCompatibilityKey: true
|
||||
] as CFDictionary
|
||||
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
let cvret = CVPixelBufferCreate(
|
||||
kCFAllocatorDefault,
|
||||
Int(size.width),
|
||||
Int(size.height),
|
||||
kCVPixelFormatType_32BGRA,
|
||||
attrs,
|
||||
&pixelBuffer
|
||||
)
|
||||
assert(cvret == kCVReturnSuccess, "CVPixelBufferCreate")
|
||||
|
||||
return pixelBuffer!
|
||||
}
|
||||
|
||||
private static func disposePixelBuffer(_ pixelBuffer: CVPixelBuffer) {
|
||||
// 'CVPixelBufferRelease' is unavailable: Core Foundation objects are
|
||||
// automatically memory managed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
public protocol UtilsProtocol: NSObject {
|
||||
func enterNativeFullscreen()
|
||||
func exitNativeFullscreen()
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
// This class creates and manipulates the different types of FlutterTexture,
|
||||
// handles resizing, rendering calls, and notify Flutter when a new frame is
|
||||
// available to render.
|
||||
//
|
||||
// To improve the user experience, a worker is used to execute heavy tasks on a
|
||||
// dedicated thread.
|
||||
public class VideoOutput: NSObject {
|
||||
// Will be called on the main thread
|
||||
public typealias TextureUpdateCallback = (Int64, CGSize) -> Void
|
||||
|
||||
private static let isSimulator: Bool = {
|
||||
let isSim: Bool
|
||||
#if targetEnvironment(simulator)
|
||||
isSim = true
|
||||
#else
|
||||
isSim = false
|
||||
#endif
|
||||
return isSim
|
||||
}()
|
||||
|
||||
private let handle: OpaquePointer
|
||||
private let enableHardwareAcceleration: Bool
|
||||
private let registry: FlutterTextureRegistry
|
||||
private let textureUpdateCallback: TextureUpdateCallback
|
||||
private let worker: Worker = .init()
|
||||
private var width: Int64?
|
||||
private var height: Int64?
|
||||
private var texture: ResizableTextureProtocol!
|
||||
private var textureId: Int64 = -1
|
||||
private var currentSize: CGSize = CGSize.zero
|
||||
private var disposed: Bool = false
|
||||
|
||||
init(
|
||||
handle: Int64,
|
||||
configuration: VideoOutputConfiguration,
|
||||
registry: FlutterTextureRegistry,
|
||||
textureUpdateCallback: @escaping TextureUpdateCallback
|
||||
) {
|
||||
let handle = OpaquePointer(bitPattern: Int(handle))
|
||||
assert(handle != nil, "handle casting")
|
||||
|
||||
self.handle = handle!
|
||||
width = configuration.width
|
||||
height = configuration.height
|
||||
enableHardwareAcceleration = configuration.enableHardwareAcceleration
|
||||
self.registry = registry
|
||||
self.textureUpdateCallback = textureUpdateCallback
|
||||
|
||||
super.init()
|
||||
|
||||
worker.enqueue {
|
||||
self._init()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
worker.cancel()
|
||||
|
||||
disposed = true
|
||||
disposeTextureId()
|
||||
}
|
||||
|
||||
public func setSize(width: Int64?, height: Int64?) {
|
||||
worker.enqueue {
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
private func _init() {
|
||||
let enableHardwareAcceleration =
|
||||
VideoOutput.isSimulator ? false : enableHardwareAcceleration
|
||||
|
||||
NSLog(
|
||||
"VideoOutput: enableHardwareAcceleration: \(enableHardwareAcceleration)"
|
||||
)
|
||||
|
||||
if VideoOutput.isSimulator {
|
||||
NSLog(
|
||||
"VideoOutput: warning: hardware rendering is disabled in the iOS simulator, due to an incompatibility with OpenGL ES"
|
||||
)
|
||||
}
|
||||
|
||||
if enableHardwareAcceleration {
|
||||
texture = SafeResizableTexture(
|
||||
TextureHW(
|
||||
handle: handle,
|
||||
// Use `weak self` to prevent memory leaks
|
||||
updateCallback: { [weak self]() in
|
||||
guard let that = self else {
|
||||
return
|
||||
}
|
||||
that.updateCallback()
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
texture = SafeResizableTexture(
|
||||
TextureSW(
|
||||
handle: handle,
|
||||
// Use `weak self` to prevent memory leaks
|
||||
updateCallback: { [weak self]() in
|
||||
guard let that = self else {
|
||||
return
|
||||
}
|
||||
that.updateCallback()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
DispatchQueue.main.sync { [weak self]() in
|
||||
guard let that = self else {
|
||||
return
|
||||
}
|
||||
that.registerTextureId()
|
||||
}
|
||||
}
|
||||
|
||||
// Must be run on the main thread
|
||||
private func registerTextureId() {
|
||||
// Textures must be registered on the platform thread.
|
||||
textureId = registry.register(texture)
|
||||
// textureUpdateCallback must run on the main thread
|
||||
textureUpdateCallback(textureId, CGSize(width: 0, height: 0))
|
||||
}
|
||||
|
||||
private func disposeTextureId() {
|
||||
let registry_ = self.registry
|
||||
let textureId_ = self.textureId
|
||||
textureId = -1
|
||||
DispatchQueue.main.async {
|
||||
// Textures must be unregistered on the platform thread
|
||||
registry_.unregisterTexture(textureId_)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateCallback() {
|
||||
worker.enqueue {
|
||||
self._updateCallback()
|
||||
}
|
||||
}
|
||||
|
||||
private func _updateCallback() {
|
||||
let size = videoSize
|
||||
|
||||
if size.width == 0 || size.height == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if currentSize != size {
|
||||
currentSize = size
|
||||
|
||||
texture.resize(size)
|
||||
DispatchQueue.main.sync { [weak self] in
|
||||
guard let that = self else { return }
|
||||
// textureUpdateCallback must run on the main thread
|
||||
that.textureUpdateCallback(that.textureId, size)
|
||||
}
|
||||
}
|
||||
|
||||
if disposed {
|
||||
return
|
||||
}
|
||||
|
||||
texture.render(size)
|
||||
DispatchQueue.main.sync { [weak self] in
|
||||
guard let that = self else { return }
|
||||
// Textures must be marked as available from the main thread
|
||||
that.registry.textureFrameAvailable(that.textureId)
|
||||
}
|
||||
}
|
||||
|
||||
private var videoSize: CGSize {
|
||||
// fixed size
|
||||
if width != nil && height != nil {
|
||||
return CGSize(
|
||||
width: Double(width!),
|
||||
height: Double(height!)
|
||||
)
|
||||
}
|
||||
|
||||
let params = MPVHelpers.getVideoOutParams(handle)
|
||||
return CGSize(
|
||||
width: Double(width ?? (params.rotate == 0 || params.rotate == 180
|
||||
? params.dw
|
||||
: params.dh)),
|
||||
height: Double(height ?? (params.rotate == 0 || params.rotate == 180
|
||||
? params.dh
|
||||
: params.dw))
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
public class VideoOutputConfiguration {
|
||||
public let width: Int64?
|
||||
public let height: Int64?
|
||||
public let enableHardwareAcceleration: Bool
|
||||
|
||||
init(
|
||||
width: Int64?,
|
||||
height: Int64?,
|
||||
enableHardwareAcceleration: Bool
|
||||
) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.enableHardwareAcceleration = enableHardwareAcceleration
|
||||
}
|
||||
|
||||
public static func fromDict(_ dict: [String: Any])
|
||||
-> VideoOutputConfiguration
|
||||
{
|
||||
let widthStr = dict["width"] as! String
|
||||
let heightStr = dict["height"] as! String
|
||||
let enableHardwareAcceleration =
|
||||
dict["enableHardwareAcceleration"] as! Bool
|
||||
|
||||
let width: Int64? = Int64(widthStr)
|
||||
let height: Int64? = Int64(heightStr)
|
||||
|
||||
return VideoOutputConfiguration(
|
||||
width: width,
|
||||
height: height,
|
||||
enableHardwareAcceleration: enableHardwareAcceleration
|
||||
)
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public class VideoOutputManager: NSObject {
|
||||
private let registry: FlutterTextureRegistry
|
||||
private var videoOutputs = [Int64: VideoOutput]()
|
||||
|
||||
init(registry: FlutterTextureRegistry) {
|
||||
self.registry = registry
|
||||
}
|
||||
|
||||
public func create(
|
||||
handle: Int64,
|
||||
configuration: VideoOutputConfiguration,
|
||||
textureUpdateCallback: @escaping VideoOutput.TextureUpdateCallback
|
||||
) {
|
||||
let videoOutput = VideoOutput(
|
||||
handle: handle,
|
||||
configuration: configuration,
|
||||
registry: self.registry,
|
||||
textureUpdateCallback: textureUpdateCallback
|
||||
)
|
||||
|
||||
self.videoOutputs[handle] = videoOutput
|
||||
}
|
||||
|
||||
public func setSize(
|
||||
handle: Int64,
|
||||
width: Int64?,
|
||||
height: Int64?
|
||||
) {
|
||||
let videoOutput = self.videoOutputs[handle]
|
||||
if videoOutput == nil {
|
||||
return
|
||||
}
|
||||
|
||||
videoOutput!.setSize(
|
||||
width: width,
|
||||
height: height
|
||||
)
|
||||
}
|
||||
|
||||
public func destroy(
|
||||
handle: Int64
|
||||
) {
|
||||
let videoOutput = self.videoOutputs[handle]
|
||||
if videoOutput == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.videoOutputs[handle] = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// https://stackoverflow.com/questions/49043257/how-to-ensure-to-run-some-code-on-same-background-thread/49075382#49075382
|
||||
class Worker {
|
||||
public typealias Job = () -> Void
|
||||
|
||||
private let semaphore = DispatchSemaphore(value: 0)
|
||||
private let lock = NSRecursiveLock()
|
||||
private var thread: Thread!
|
||||
private var queue = [Job]()
|
||||
private var canceled: Bool = false
|
||||
|
||||
init() {
|
||||
thread = Thread(block: loop)
|
||||
thread.start()
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
signalCancel()
|
||||
thread.cancel()
|
||||
}
|
||||
|
||||
public func enqueue(_ job: @escaping Job) {
|
||||
locked {
|
||||
queue.append(job)
|
||||
}
|
||||
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
private func loop() {
|
||||
while true {
|
||||
semaphore.wait()
|
||||
|
||||
if isCanceled() {
|
||||
return
|
||||
}
|
||||
|
||||
let job = getFirstJob()
|
||||
job()
|
||||
}
|
||||
}
|
||||
|
||||
private func signalCancel() {
|
||||
locked {
|
||||
canceled = true
|
||||
}
|
||||
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
private func isCanceled() -> Bool {
|
||||
let c = locked {
|
||||
canceled
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
private func getFirstJob() -> Job {
|
||||
let job = locked {
|
||||
queue.removeFirst()
|
||||
}
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
private func locked<T>(do block: () -> T) -> T {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return block()
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with _: FlutterPluginRegistrar) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
SPACE := $(subst ,, )
|
||||
HEADERS_DESTDIR_ESCAPED=$(subst $(SPACE),\ ,${HEADERS_DESTDIR})
|
||||
|
||||
all: ${HEADERS_DESTDIR_ESCAPED}/mpv/*.h
|
||||
|
||||
MPV_HEADERS_VERSION=v0.36.0
|
||||
MPV_HEADERS_SHA256SUM=29abc44f8ebee013bb2f9fe14d80b30db19b534c679056e4851ceadf5a5e8bf6
|
||||
|
||||
TAR=tar
|
||||
TAR_WILDCARDS_FLAG := $(shell ${TAR} --version 2>&1 | grep -q 'GNU' && echo "--wildcards" || echo "")
|
||||
|
||||
.cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz:
|
||||
mkdir -p .cache/headers
|
||||
rm -f .cache/headers/*.tmp .cache/headers/*.tar.gz
|
||||
curl -L \
|
||||
https://github.com/mpv-player/mpv/archive/refs/tags/${MPV_HEADERS_VERSION}.tar.gz \
|
||||
-o .cache/headers/mpv.tar.gz.tmp
|
||||
shasum -a 256 -c <<< '${MPV_HEADERS_SHA256SUM} .cache/headers/mpv.tar.gz.tmp'
|
||||
mv .cache/headers/mpv.tar.gz.tmp .cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz
|
||||
touch .cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz
|
||||
|
||||
.cache/headers/mpv.tar.gz: .cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz
|
||||
rm -f .cache/headers/mpv.tar.gz
|
||||
ln -s mpv-${MPV_HEADERS_VERSION}.tar.gz .cache/headers/mpv.tar.gz
|
||||
|
||||
${HEADERS_DESTDIR_ESCAPED}/mpv/*.h: .cache/headers/mpv.tar.gz
|
||||
mkdir -p ${HEADERS_DESTDIR_ESCAPED}/mpv
|
||||
${TAR} ${TAR_WILDCARDS_FLAG} -xvf .cache/headers/mpv.tar.gz --strip-components 2 -C ${HEADERS_DESTDIR_ESCAPED}/mpv/ 'mpv-*/libmpv/*.h'
|
||||
touch ${HEADERS_DESTDIR_ESCAPED}/mpv/*.h
|
||||
@@ -0,0 +1,95 @@
|
||||
class MediaKitUtils
|
||||
attr_accessor :libs_found, :libs_package
|
||||
|
||||
module Platform
|
||||
IOS ||= 'ios'
|
||||
MACOS ||= 'macos'
|
||||
end
|
||||
|
||||
module Type
|
||||
AUDIO ||= 'audio'
|
||||
VIDEO ||= 'video'
|
||||
end
|
||||
|
||||
def initialize(platform)
|
||||
raise "Expecting '#{Platform::IOS}' or '#{Platform::MACOS}': platform = #{platform}" if
|
||||
!(platform == Platform::IOS || platform == Platform::MACOS)
|
||||
|
||||
# Find the nearest path to `pubspec.lock` using `PWD` env var set by
|
||||
# `cocoapods`
|
||||
current_dir = ENV['PWD'] || '/'
|
||||
pubspec_lock_path = MediaKitUtils::find_nearest_pubspec_lock(current_dir)
|
||||
|
||||
# If not found try again using `PWD_FALLBACK` env var manually set,
|
||||
# `PWD_FALLBACK` must point to app folder or any child folder
|
||||
if pubspec_lock_path == ''
|
||||
current_dir = ENV['PWD_FALLBACK'] || '/'
|
||||
pubspec_lock_path = MediaKitUtils::find_nearest_pubspec_lock(current_dir)
|
||||
end
|
||||
|
||||
# Abort if no `pubspec.lock` was found
|
||||
if pubspec_lock_path == ''
|
||||
abort(
|
||||
sprintf(
|
||||
'media_kit: ERROR: No pubspec.lock was found: ENV["PWD"] = "%s"',
|
||||
ENV["PWD"]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Load packages from `pubspec.lock`
|
||||
pubspec_lock = YAML.load_file(pubspec_lock_path)
|
||||
packages = pubspec_lock['packages']
|
||||
|
||||
# Checks for `media_kit_libs_***` in `pubspec.lock`
|
||||
libs_count = 0
|
||||
@libs_package = ''
|
||||
Type.constants.each do |constant|
|
||||
type = Type.const_get(constant)
|
||||
package = sprintf('media_kit_libs_%s_%s', platform, type)
|
||||
if packages.keys.include?(package)
|
||||
libs_count += 1
|
||||
@libs_package = package
|
||||
|
||||
puts sprintf('media_kit: INFO: package:%s found', package)
|
||||
end
|
||||
end
|
||||
|
||||
# Abort if multiple `media_kit_libs_***` was found
|
||||
if libs_count > 1
|
||||
abort(
|
||||
sprintf(
|
||||
'media_kit: ERROR: package:media_kit_libs_%s_*** must be uniq',
|
||||
platform,
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@libs_found = libs_count > 0
|
||||
|
||||
# Warn if no `media_kit_libs_*` was found
|
||||
if !@libs_found
|
||||
warn(
|
||||
sprintf(
|
||||
'media_kit: WARNING: package:media_kit_libs_%s_*** not found',
|
||||
platform
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Looks for the nearest `pubspec.lock` by recursively ascending through the
|
||||
# parent folders
|
||||
def self.find_nearest_pubspec_lock(current_dir)
|
||||
while current_dir != '/'
|
||||
path = File.join(current_dir, 'pubspec.lock')
|
||||
if File.exist?(path)
|
||||
return path
|
||||
else
|
||||
current_dir = File.expand_path('..', current_dir)
|
||||
end
|
||||
end
|
||||
|
||||
return ''
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user