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,205 @@
import Flutter
import OpenGLES
public class TextureHW: NSObject, FlutterTexture, ResizableTextureProtocol {
public typealias UpdateCallback = () -> Void
private let handle: OpaquePointer
private let updateCallback: UpdateCallback
private let context: EAGLContext
private let textureCache: CVOpenGLESTextureCache
private var renderContext: OpaquePointer?
private var textureContexts = SwappableObjectManager<TextureGLESContext>(
objects: [],
skipCheckArgs: true
)
init(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
self.handle = handle
self.updateCallback = updateCallback
self.context = OpenGLESHelpers.createContext()
self.textureCache = OpenGLESHelpers.createTextureCache(context)
super.init()
self.initMPV()
}
deinit {
disposePixelBuffer()
disposeMPV()
OpenGLESHelpers.deleteTextureCache(textureCache)
// Deleting the context may cause potential RAM or VRAM memory leaks, as it
// is used in the `deinit` method of the `TextureGLESContext`.
// Potential fix: use a counter, and delete it only when the counter reaches
// zero
OpenGLESHelpers.deleteContext(context)
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
let textureContext = textureContexts.current
if textureContext == nil {
return nil
}
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}
private func initMPV() {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("initMPV")
EAGLContext.setCurrent(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() {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("disposeMPV")
EAGLContext.setCurrent(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: [
TextureGLESContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLESContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLESContext(
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
}
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("render")
EAGLContext.setCurrent(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.opengles" as CFString
)
let addr = CFBundleGetFunctionPointerForName(indentifier, symbol)
if addr == nil {
NSLog("Cannot get OpenGLES function pointer!")
}
return addr
}
}
@@ -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,181 @@
import OpenGLES
import UIKit
public class OpenGLESHelpers {
static public func createContext() -> EAGLContext {
let context = EAGLContext(api: .openGLES3)
return context!
}
static public func createTextureCache(
_ context: EAGLContext
) -> CVOpenGLESTextureCache {
var textureCache: CVOpenGLESTextureCache?
let cvret: CVReturn = CVOpenGLESTextureCacheCreate(
kCFAllocatorDefault,
nil,
context,
nil,
&textureCache
)
assert(cvret == kCVReturnSuccess, "CVOpenGLESTextureCacheCreate")
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: CVOpenGLESTextureCache,
_ pixelBuffer: CVPixelBuffer,
_ size: CGSize
) -> CVOpenGLESTexture {
var texture: CVOpenGLESTexture?
let cvret: CVReturn = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
textureCache,
pixelBuffer,
nil,
GLenum(GL_TEXTURE_2D),
GL_RGBA,
GLsizei(size.width),
GLsizei(size.height),
GLenum(GL_BGRA),
GLenum(GL_UNSIGNED_BYTE),
0,
&texture
)
assert(
cvret == kCVReturnSuccess,
"CVOpenGLESTextureCacheCreateTextureFromImage"
)
return texture!
}
public enum FrameBufferError: Error {
case status(status: GLuint)
}
static public func createFrameBuffer(
context: EAGLContext,
texture: CVOpenGLESTexture,
size: CGSize
) throws -> GLuint {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("createFrameBuffer")
EAGLContext.setCurrent(nil)
}
let textureName: GLuint = CVOpenGLESTextureGetName(texture)
glBindTexture(GLenum(GL_TEXTURE_2D), textureName)
defer {
glBindTexture(GLenum(GL_TEXTURE_2D), 0)
}
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_2D),
textureName,
0
)
let status = glCheckFramebufferStatus(GLenum(GL_FRAMEBUFFER))
if status != GLenum(GL_FRAMEBUFFER_COMPLETE) {
glDeleteFramebuffers(1, &frameBuffer)
throw OpenGLESHelpers.FrameBufferError.status(status: status)
}
return frameBuffer
}
static public func deleteContext(_ context: EAGLContext) {
// Release function for EAGLContext not found
}
static public func deleteTextureCache(_ textureCache: CVOpenGLESTextureCache) {
CVOpenGLESTextureCacheFlush(textureCache, 0)
// 'CVOpenGLESTextureCacheRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
static public func deletePixeBuffer(
_ context: EAGLContext,
_ pixelBuffer: CVPixelBuffer
) {
// 'CVPixelBufferRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
// BUG: `glDeleteTextures` does not release `CVOpenGLESTexture`.
// `CVOpenGLESTextureCache` retains a direct or indirect reference to
// `IOSurface`, which causes a memory leak until `CVOpenGLESTextureCache` is
// released.
static public func deleteTexture(
_ context: EAGLContext,
_ texture: CVOpenGLESTexture
) {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("deleteTexture")
EAGLContext.setCurrent(nil)
}
var textureName: GLuint = CVOpenGLESTextureGetName(texture)
glDeleteTextures(1, &textureName)
}
static public func deleteFrameBuffer(
_ context: EAGLContext,
_ frameBuffer: GLuint
) {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("deleteFrameBuffer")
EAGLContext.setCurrent(nil)
}
var frameBuffer = frameBuffer
glDeleteFramebuffers(1, &frameBuffer)
}
static public func checkError(_ message: String) {
let error = glGetError()
if error == GL_NO_ERROR {
return
}
NSLog("OpenGLESHelpers: error: \(message): \(error)")
}
}
@@ -0,0 +1,54 @@
public class TextureGLESContext {
private let context: EAGLContext
public let frameBuffer: GLuint
public let texture: CVOpenGLESTexture
public let pixelBuffer: CVPixelBuffer
init(
context: EAGLContext,
textureCache: CVOpenGLESTextureCache,
size: CGSize
) {
self.context = context
// Sometimes the frameBuffer created by `createFrameBuffer` has the status
// `GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT`, which makes it unusable.
// This seems to be because the pixelBuffer created by `createPixelBuffer`
// appears to be invalid, with no way to prevent or detect this.
// So we have to try several times to create a frameBuffer valid.
// This is usually the case after the second attempt.
for index in 0...3 {
let pixelBuffer = OpenGLESHelpers.createPixelBuffer(size)
let texture = OpenGLESHelpers.createTexture(
textureCache,
pixelBuffer,
size
)
let frameBuffer = try? OpenGLESHelpers.createFrameBuffer(
context: context,
texture: texture,
size: size
)
if frameBuffer != nil {
self.pixelBuffer = pixelBuffer
self.texture = texture
self.frameBuffer = frameBuffer!
return
}
OpenGLESHelpers.deletePixeBuffer(context, pixelBuffer)
OpenGLESHelpers.deleteTexture(context, texture)
}
NSLog("TextureGLESContext: init: unable to create a valid frameBuffer")
exit(1)
}
deinit {
OpenGLESHelpers.deletePixeBuffer(context, pixelBuffer)
OpenGLESHelpers.deleteTexture(context, texture)
OpenGLESHelpers.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) {}
}