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,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, ¶ms)
|
||||
)
|
||||
|
||||
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, ¶ms)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+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) {}
|
||||
}
|
||||
Reference in new issue
Block a user