Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)
ci / core (push) Successful in 28s
ci / server (push) Successful in 51s
ci / updater-windows (push) Failing after 5m59s

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:
foxyzandClaude Opus 5.5 committed 2026-10-16 01:13:14 +03:00
1 parent 25b19f2d6a
commit a87ee98faa
131 files changed
+15532 -6

No files matched your search

@@ -0,0 +1,208 @@
import FlutterMacOS
import OpenGL.GL
import OpenGL.GL3
public class TextureHW: NSObject, FlutterTexture, ResizableTextureProtocol {
public typealias UpdateCallback = () -> Void
private let handle: OpaquePointer
private let updateCallback: UpdateCallback
private let pixelFormat: CGLPixelFormatObj
private let context: CGLContextObj
private let textureCache: CVOpenGLTextureCache
private var renderContext: OpaquePointer?
private var textureContexts = SwappableObjectManager<TextureGLContext>(
objects: [],
skipCheckArgs: true
)
init(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
self.handle = handle
self.updateCallback = updateCallback
self.pixelFormat = OpenGLHelpers.createPixelFormat()
self.context = OpenGLHelpers.createContext(pixelFormat)
self.textureCache = OpenGLHelpers.createTextureCache(context, pixelFormat)
super.init()
self.initMPV()
}
deinit {
disposePixelBuffer()
disposeMPV()
OpenGLHelpers.deleteTextureCache(textureCache)
OpenGLHelpers.deletePixelFormat(pixelFormat)
// Deleting the context may cause potential RAM or VRAM memory leaks, as it
// is used in the `deinit` method of the `TextureGLContext`.
// Potential fix: use a counter, and delete it only when the counter reaches
// zero
OpenGLHelpers.deleteContext(context)
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
let textureContext = textureContexts.current
if textureContext == nil {
return nil
}
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}
private func initMPV() {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("initMPV")
CGLSetCurrentContext(nil)
}
let api = UnsafeMutableRawPointer(
mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String
)
var procAddress = mpv_opengl_init_params(
get_proc_address: {
(ctx, name) in
return TextureHW.getProcAddress(ctx, name)
},
get_proc_address_ctx: nil
)
var params: [mpv_render_param] = withUnsafeMutableBytes(of: &procAddress) {
procAddress in
return [
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
mpv_render_param(
type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS,
data: procAddress.baseAddress.map {
UnsafeMutableRawPointer($0)
}
),
mpv_render_param(),
]
}
MPVHelpers.checkError(
mpv_render_context_create(&renderContext, handle, &params)
)
mpv_render_context_set_update_callback(
renderContext,
{ (ctx) in
let that = unsafeBitCast(ctx, to: TextureHW.self)
DispatchQueue.main.async {
that.updateCallback()
}
},
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
private func disposeMPV() {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("disposeMPV")
CGLSetCurrentContext(nil)
}
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("TextureGL: resize: \(size.width)x\(size.height)")
createPixelBuffer(size)
}
private func createPixelBuffer(_ size: CGSize) {
disposePixelBuffer()
textureContexts.reinit(
objects: [
TextureGLContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLContext(
context: context,
textureCache: textureCache,
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
}
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("render")
CGLSetCurrentContext(nil)
}
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), textureContext!.frameBuffer)
defer {
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
}
var fbo = mpv_opengl_fbo(
fbo: Int32(textureContext!.frameBuffer),
w: Int32(size.width),
h: Int32(size.height),
internal_format: 0
)
let fboPtr = withUnsafeMutablePointer(to: &fbo) { $0 }
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: fboPtr),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
mpv_render_context_render(renderContext, &params)
glFlush()
textureContexts.pushAsReady(textureContext!)
}
static private func getProcAddress(
_ ctx: UnsafeMutableRawPointer?,
_ name: UnsafePointer<Int8>?
) -> UnsafeMutableRawPointer? {
let symbol: CFString = CFStringCreateWithCString(
kCFAllocatorDefault,
name,
kCFStringEncodingASCII
)
let indentifier = CFBundleGetBundleWithIdentifier(
"com.apple.opengl" as CFString
)
let addr = CFBundleGetFunctionPointerForName(indentifier, symbol)
if addr == nil {
NSLog("Cannot get OpenGL function pointer!")
}
return addr
}
}
@@ -0,0 +1,37 @@
import FlutterMacOS
public class Utils: NSObject, UtilsProtocol {
private let registrar: FlutterPluginRegistrar
init(_ registrar: FlutterPluginRegistrar) {
self.registrar = registrar
}
private var window: NSWindow? {
registrar.view?.window
}
public func enterNativeFullscreen() {
guard let window = window else {
return printWarning()
}
if !window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
}
public func exitNativeFullscreen() {
guard let window = window else {
return printWarning()
}
if window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
}
private func printWarning() {
NSLog("Utils: warning: unable to find the window")
}
}
@@ -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)
}
}
@@ -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
)
}
}
@@ -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)
}
}
@@ -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)
}
@@ -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()
}
}
@@ -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, &params)
)
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, &params)
textureContexts.pushAsReady(textureContext!)
}
}
@@ -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))
)
}
}
@@ -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
)
}
}
@@ -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()
}
}
@@ -0,0 +1,264 @@
import Cocoa
import OpenGL.GL
import OpenGL.GL3
public class OpenGLHelpers {
static public func createPixelFormat() -> CGLPixelFormatObj {
// from mpv
let attributes: [CGLPixelFormatAttribute] = [
kCGLPFAOpenGLProfile,
CGLPixelFormatAttribute(kCGLOGLPVersion_3_2_Core.rawValue),
kCGLPFAAccelerated,
kCGLPFADoubleBuffer,
kCGLPFAColorSize, _CGLPixelFormatAttribute(rawValue: 64),
kCGLPFAColorFloat,
kCGLPFABackingStore,
kCGLPFAAllowOfflineRenderers,
kCGLPFASupportsAutomaticGraphicsSwitching,
_CGLPixelFormatAttribute(rawValue: 0),
]
var npix: GLint = 0
var pixelFormat: CGLPixelFormatObj?
CGLChoosePixelFormat(attributes, &pixelFormat, &npix)
return pixelFormat!
}
static public func createContext(
_ pixelFormat: CGLPixelFormatObj
) -> CGLContextObj {
var context: CGLContextObj?
let error = CGLCreateContext(pixelFormat, nil, &context)
if error != kCGLNoError {
let errS = String(cString: CGLErrorString(error))
NSLog(errS)
exit(1)
}
return context!
}
static public func createTextureCache(
_ context: CGLContextObj,
_ pixelFormat: CGLPixelFormatObj
) -> CVOpenGLTextureCache {
var textureCache: CVOpenGLTextureCache?
let cvret: CVReturn = CVOpenGLTextureCacheCreate(
kCFAllocatorDefault,
nil,
context,
pixelFormat,
nil,
&textureCache
)
assert(cvret == kCVReturnSuccess, "CVOpenGLTextureCacheCreate")
return textureCache!
}
static public func createPixelBuffer(_ size: CGSize) -> CVPixelBuffer {
var pixelBuffer: CVPixelBuffer?
let attrs =
[
kCVPixelBufferMetalCompatibilityKey: true
] as CFDictionary
let cvret: CVReturn = CVPixelBufferCreate(
kCFAllocatorDefault,
Int(size.width),
Int(size.height),
kCVPixelFormatType_32BGRA,
attrs,
&pixelBuffer
)
assert(cvret == kCVReturnSuccess, "CVPixelBufferCreate")
return pixelBuffer!
}
static public func createTexture(
_ textureCache: CVOpenGLTextureCache,
_ pixelBuffer: CVPixelBuffer
) -> CVOpenGLTexture {
var texture: CVOpenGLTexture?
let cvret: CVReturn = CVOpenGLTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
textureCache,
pixelBuffer,
nil,
&texture
)
assert(
cvret == kCVReturnSuccess,
"CVOpenGLTextureCacheCreateTextureFromImage"
)
return texture!
}
static public func createRenderBuffer(
_ context: CGLContextObj,
_ size: CGSize
) -> GLuint {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("createRenderBuffer")
CGLSetCurrentContext(nil)
}
var renderBuffer: GLuint = GLuint()
glGenRenderbuffers(1, &renderBuffer)
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), renderBuffer)
defer {
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), 0)
}
glRenderbufferStorage(
GLenum(GL_RENDERBUFFER),
GLenum(GL_DEPTH24_STENCIL8),
GLsizei(size.width),
GLsizei(size.height)
)
return renderBuffer
}
static public func createFrameBuffer(
context: CGLContextObj,
renderBuffer: GLuint,
texture: CVOpenGLTexture,
size: CGSize
) -> GLuint {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("createFrameBuffer")
CGLSetCurrentContext(nil)
}
let textureName: GLuint = CVOpenGLTextureGetName(texture)
glBindTexture(GLenum(GL_TEXTURE_RECTANGLE), textureName)
defer {
glBindTexture(GLenum(GL_TEXTURE_RECTANGLE), 0)
}
glTexParameteri(
GLenum(GL_TEXTURE_RECTANGLE),
GLenum(GL_TEXTURE_MAG_FILTER),
GL_LINEAR
)
glTexParameteri(
GLenum(GL_TEXTURE_RECTANGLE),
GLenum(GL_TEXTURE_MIN_FILTER),
GL_LINEAR
)
glViewport(0, 0, GLsizei(size.width), GLsizei(size.height))
var frameBuffer: GLuint = 0
glGenFramebuffers(1, &frameBuffer)
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
defer {
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
}
glFramebufferTexture2D(
GLenum(GL_FRAMEBUFFER),
GLenum(GL_COLOR_ATTACHMENT0),
GLenum(GL_TEXTURE_RECTANGLE),
textureName,
0
)
glFramebufferRenderbuffer(
GLenum(GL_FRAMEBUFFER),
GLenum(GL_DEPTH_ATTACHMENT),
GLenum(GL_RENDERBUFFER),
renderBuffer
)
return frameBuffer
}
static public func deletePixelFormat(_ pixelFormat: CGLPixelFormatObj) {
CGLReleasePixelFormat(pixelFormat)
}
static public func deleteContext(_ context: CGLContextObj) {
CGLSetCurrentContext(nil)
CGLReleaseContext(context)
}
static public func deleteTextureCache(_ textureCache: CVOpenGLTextureCache) {
CVOpenGLTextureCacheFlush(textureCache, 0)
// 'CVOpenGLTextureCacheRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
static public func deletePixeBuffer(
_ context: CGLContextObj,
_ pixelBuffer: CVPixelBuffer
) {
// 'CVPixelBufferRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
// BUG: `glDeleteTextures` does not release `CVOpenGLTexture`.
// `CVOpenGLTextureCache` retains a direct or indirect reference to
// `IOSurface`, which causes a memory leak until `CVOpenGLTextureCache` is
// released.
static public func deleteTexture(
_ context: CGLContextObj,
_ texture: CVOpenGLTexture
) {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("deleteTexture")
CGLSetCurrentContext(nil)
}
var textureName: GLuint = CVOpenGLTextureGetName(texture)
glDeleteTextures(1, &textureName)
}
static public func deleteRenderBuffer(
_ context: CGLContextObj,
_ renderBuffer: GLuint
) {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("deleteRenderBuffer")
CGLSetCurrentContext(nil)
}
var renderBuffer = renderBuffer
glDeleteRenderbuffers(1, &renderBuffer)
}
static public func deleteFrameBuffer(
_ context: CGLContextObj,
_ frameBuffer: GLuint
) {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("deleteFrameBuffer")
CGLSetCurrentContext(nil)
}
var frameBuffer = frameBuffer
glDeleteFramebuffers(1, &frameBuffer)
}
static public func checkError(_ message: String) {
let error = glGetError()
if error == GL_NO_ERROR {
return
}
NSLog("OpenGLHelpers: error: \(message): \(error)")
}
}
@@ -0,0 +1,41 @@
public class TextureGLContext {
private let context: CGLContextObj
private let renderBuffer: GLuint
public let frameBuffer: GLuint
public let texture: CVOpenGLTexture
public let pixelBuffer: CVPixelBuffer
init(
context: CGLContextObj,
textureCache: CVOpenGLTextureCache,
size: CGSize
) {
self.context = context
self.pixelBuffer = OpenGLHelpers.createPixelBuffer(size)
self.texture = OpenGLHelpers.createTexture(
textureCache,
pixelBuffer
)
self.renderBuffer = OpenGLHelpers.createRenderBuffer(
context,
size
)
self.frameBuffer = OpenGLHelpers.createFrameBuffer(
context: context,
renderBuffer: renderBuffer,
texture: texture,
size: size
)
}
deinit {
OpenGLHelpers.deletePixeBuffer(context, pixelBuffer)
OpenGLHelpers.deleteTexture(context, texture)
OpenGLHelpers.deleteRenderBuffer(context, renderBuffer)
OpenGLHelpers.deleteFrameBuffer(context, frameBuffer)
}
}
@@ -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) {}
}