From 4d7638ab0f2f841de4992ca519d6556e5b650a44 Mon Sep 17 00:00:00 2001 From: wm4 Date: Thu, 10 Mar 2016 21:45:48 +0100 Subject: Reimport client API examples from mpv main repository From commit d8b27ee4de4e26d594855960a4421b99db9e76ea. --- libmpv/Copyright | 14 ++ libmpv/README.md | 114 +++++++++++++ libmpv/cocoa-openglcb/cocoa-openglcb.m | 304 +++++++++++++++++++++++++++++++++ libmpv/cocoa/cocoabasic.m | 209 +++++++++++++++++++++++ libmpv/qml/main.cpp | 136 +++++++++++++++ libmpv/qml/main.h | 36 ++++ libmpv/qml/main.qml | 71 ++++++++ libmpv/qml/mpvtest.pro | 12 ++ libmpv/qml/mpvtest.qrc | 5 + libmpv/qml_direct/main.cpp | 159 +++++++++++++++++ libmpv/qml_direct/main.h | 51 ++++++ libmpv/qml_direct/main.qml | 49 ++++++ libmpv/qml_direct/mpvtest.pro | 12 ++ libmpv/qml_direct/mpvtest.qrc | 5 + libmpv/qt/qtexample.cpp | 225 ++++++++++++++++++++++++ libmpv/qt/qtexample.h | 37 ++++ libmpv/qt/qtexample.pro | 13 ++ libmpv/qt_opengl/main.cpp | 13 ++ libmpv/qt_opengl/mainwindow.cpp | 52 ++++++ libmpv/qt_opengl/mainwindow.h | 27 +++ libmpv/qt_opengl/mpvwidget.cpp | 129 ++++++++++++++ libmpv/qt_opengl/mpvwidget.h | 38 +++++ libmpv/qt_opengl/qt_opengl.pro | 13 ++ libmpv/sdl/main.c | 158 +++++++++++++++++ libmpv/simple/simple.c | 54 ++++++ 25 files changed, 1936 insertions(+) create mode 100644 libmpv/Copyright create mode 100644 libmpv/README.md create mode 100644 libmpv/cocoa-openglcb/cocoa-openglcb.m create mode 100644 libmpv/cocoa/cocoabasic.m create mode 100644 libmpv/qml/main.cpp create mode 100644 libmpv/qml/main.h create mode 100644 libmpv/qml/main.qml create mode 100644 libmpv/qml/mpvtest.pro create mode 100644 libmpv/qml/mpvtest.qrc create mode 100644 libmpv/qml_direct/main.cpp create mode 100644 libmpv/qml_direct/main.h create mode 100644 libmpv/qml_direct/main.qml create mode 100644 libmpv/qml_direct/mpvtest.pro create mode 100644 libmpv/qml_direct/mpvtest.qrc create mode 100644 libmpv/qt/qtexample.cpp create mode 100644 libmpv/qt/qtexample.h create mode 100644 libmpv/qt/qtexample.pro create mode 100644 libmpv/qt_opengl/main.cpp create mode 100644 libmpv/qt_opengl/mainwindow.cpp create mode 100644 libmpv/qt_opengl/mainwindow.h create mode 100644 libmpv/qt_opengl/mpvwidget.cpp create mode 100644 libmpv/qt_opengl/mpvwidget.h create mode 100644 libmpv/qt_opengl/qt_opengl.pro create mode 100644 libmpv/sdl/main.c create mode 100644 libmpv/simple/simple.c diff --git a/libmpv/Copyright b/libmpv/Copyright new file mode 100644 index 0000000..38d1891 --- /dev/null +++ b/libmpv/Copyright @@ -0,0 +1,14 @@ +All examples in this directory and its sub-directories are licensed +under one of the following licenses: + + WTFPL, ISC, Ms-PL, AGPLv3, BSD (any) + +Pick any license of your liking, and disregard the others. + +(The full text of each license is available on this website: + http://opensource.org/licenses/alphabetical ) + +Additionally, you may consider the example code to be public domain. +You are free to use any of the example code without further +requirements or need for attribution. + diff --git a/libmpv/README.md b/libmpv/README.md new file mode 100644 index 0000000..c21deb5 --- /dev/null +++ b/libmpv/README.md @@ -0,0 +1,114 @@ +# Client API examples + +All these examples use the mpv client API through libmpv. + +## Where are the docs? + +The libmpv C API is documented directly in the header files (on normal Unix +systems, this is in `/usr/include/mpv/client.h`. + +libmpv merely gives you access to mpv's command interface, which is documented +here: +* Options (`mpv_set_option()` and friends): http://mpv.io/manual/master/#options +* Commands (`mpv_command()` and friends): http://mpv.io/manual/master/#list-of-input-commands +* Properties (`mpv_set_property()` and friends): http://mpv.io/manual/master/#properties + +Essentially everything is done with them, including loading a file, retrieving +playback progress, and so on. + +## Methods of embedding the video window + +All of these examples concentrate on how to integrate mpv's video renderers +with your own GUI. This is generally the hardest part. libmpv enforces a +somewhat efficient video output method, rather than e.g. returning a RGBA +surface in memory for each frame. The latter would be prohibitively inefficient, +because it would require conversion on the CPU. The goal is also not requiring +the API users to reinvent their own video rendering/scaling/presentation +mechanisms. + +There are currently 2 methods of embedding video. + +### Native window embedding + +This uses the platform's native method of nesting multiple windows. For example, +Linux/X11 can nest a window from a completely different process. The nested +window can redraw contents on its own, and receive user input if the user +interacts with this window. + +libmpv lets you specify a parent window for its own video window via the `wid` +option. Then libmpv will create its window with your window as parent, and +render its video inside of it. + +This method is highly OS-dependent. Some behavior is OS-specific. There are +problems with focusing on X11 (the ancient X11 focus policy mismatches with +that of modern UI toolkits - it's normally worked around, but this is not +easily possible with raw window embedding). It seems to have stability problems +on OSX when using the Qt toolkit. + +### OpenGL embedding + +This method lets you use libmpv's OpenGL renderer directly. You create an +OpenGL context, and then use `mpv_opengl_cb_draw()` to render the video on +each frame. + +This is more complicated, because libmpv will work directly on your own OpenGL +state. It's also not possible to have mpv automatically receive user input. +You will have to simulate this with the `mouse`/`keypress`/`keydown`/`keyup` +commands. + +You also get much more flexibility. For example, you can actually render your +own OSD on top of the video, something that is not possible with raw window +embedding. + +### Which one to use? + +Due to the various platform-specific behavior and problems (in particular on +OSX), OpenGL embedding is recommended. If you're not comfortable with requiring +OpenGL, or want to support "direct" video output such as vdpau (which might +win when it comes to performance and energy-saving), you should probably +support both methods if possible. + +## List of examples + +### simple + +Very primitive terminal-only example. Shows some most basic API usage. + +### cocoa + +Shows how to embed the mpv video window in Objective-C/Cocoa. + +### cocoa-openglcb + +Similar to the cocoa sample, but shows how to integrate mpv's OpenGL renderer +using libmpv's opengl-cb API. Since it does not require complicated interaction +with Cocoa elements from different libraries, it's more robust. + +### qt + +Shows how to embed the mpv video window in Qt (using normal desktop widgets). + +### qt_opengl + +Shows how to use mpv's OpenGL video renderer in Qt. This uses the opengl-cb API +for video. Since it does not rely on embedding "foreign" native Windows, it's +usually more robust, potentially faster, and it's easier to control how your +GUI interacts with the video. You can do your own OpenGL rendering on top of +the video as well. + +### qml + +Shows how to use mpv's OpenGL video renderer in QtQuick2 with QML. Uses the +opengl-cb API for video. Since the video is a normal QML element, it's trivial +to create OSD overlays with QML-native graphical elements as well. + +### qml_direct + +Alternative example, which typically avoids a FBO indirection. Might be +slightly faster, but is less flexible and harder to use. In particular, the +video is not a normal QML element. Uses the opengl-cb API for video. + +### sdl + +Show how to embed the mpv OpenGL renderer in SDL. Uses the opengl-cb API for +video. diff --git a/libmpv/cocoa-openglcb/cocoa-openglcb.m b/libmpv/cocoa-openglcb/cocoa-openglcb.m new file mode 100644 index 0000000..4a4e8aa --- /dev/null +++ b/libmpv/cocoa-openglcb/cocoa-openglcb.m @@ -0,0 +1,304 @@ +// Plays a video from the command line in an opengl view in its own window. + +// Build with: clang -o cocoa-openglcb cocoa-openglcb.m `pkg-config --libs --cflags mpv` -framework Cocoa -framework OpenGL + +#import +#import + +#import +#import +#import + +#import + + +static inline void check_error(int status) +{ + if (status < 0) { + printf("mpv API error: %s\n", mpv_error_string(status)); + exit(1); + } +} + +static void *get_proc_address(void *ctx, const char *name) +{ + CFStringRef symbolName = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingASCII); + void *addr = CFBundleGetFunctionPointerForName(CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl")), symbolName); + CFRelease(symbolName); + return addr; +} + +static void glupdate(void *ctx); + +@interface MpvClientOGLView : NSOpenGLView +@property mpv_opengl_cb_context *mpvGL; +- (instancetype)initWithFrame:(NSRect)frame; +- (void)drawRect; +- (void)fillBlack; +@end + +@implementation MpvClientOGLView +- (instancetype)initWithFrame:(NSRect)frame +{ + // make sure the pixel format is double buffered so we can use + // [[self openGLContext] flushBuffer]. + NSOpenGLPixelFormatAttribute attributes[] = { + NSOpenGLPFADoubleBuffer, + 0 + }; + self = [super initWithFrame:frame + pixelFormat:[[NSOpenGLPixelFormat alloc] initWithAttributes:attributes]]; + + if (self) { + [self setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; + // swap on vsyncs + GLint swapInt = 1; + [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; + [[self openGLContext] makeCurrentContext]; + self.mpvGL = nil; + } + return self; +} + +- (void)fillBlack +{ + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); +} + +- (void)drawRect +{ + if (self.mpvGL) + mpv_opengl_cb_draw(self.mpvGL, 0, self.bounds.size.width, -self.bounds.size.height); + else + [self fillBlack]; + [[self openGLContext] flushBuffer]; +} + +- (void)drawRect:(NSRect)dirtyRect +{ + [self drawRect]; +} +@end + +@interface CocoaWindow : NSWindow +@property(retain, readonly) MpvClientOGLView *glView; +@property(retain, readonly) NSButton *pauseButton; +@end + +@implementation CocoaWindow +- (BOOL)canBecomeMainWindow { return YES; } +- (BOOL)canBecomeKeyWindow { return YES; } +- (void)initOGLView { + NSRect bounds = [[self contentView] bounds]; + // window coordinate origin is bottom left + NSRect glFrame = NSMakeRect(bounds.origin.x, bounds.origin.y + 30, bounds.size.width, bounds.size.height - 30); + _glView = [[MpvClientOGLView alloc] initWithFrame:glFrame]; + [self.contentView addSubview:_glView]; + + NSRect buttonFrame = NSMakeRect(bounds.origin.x, bounds.origin.y, 60, 30); + _pauseButton = [[NSButton alloc] initWithFrame:buttonFrame]; + _pauseButton.buttonType = NSToggleButton; + // button target has to be the delegate (it holds the mpv context + // pointer), so that's set later. + _pauseButton.action = @selector(togglePause:); + _pauseButton.title = @"Pause"; + _pauseButton.alternateTitle = @"Play"; + [self.contentView addSubview:_pauseButton]; +} +@end + +@interface AppDelegate : NSObject +{ + mpv_handle *mpv; + dispatch_queue_t queue; + CocoaWindow *window; +} +@end + +static void wakeup(void *); + +@implementation AppDelegate + +- (void)createWindow { + + int mask = NSTitledWindowMask|NSClosableWindowMask| + NSMiniaturizableWindowMask|NSResizableWindowMask; + + window = [[CocoaWindow alloc] + initWithContentRect:NSMakeRect(0, 0, 1280, 720) + styleMask:mask + backing:NSBackingStoreBuffered + defer:NO]; + + // force a minimum size to stop opengl from exploding. + [window setMinSize:NSMakeSize(200, 200)]; + [window initOGLView]; + [window setTitle:@"cocoa-openglcb example"]; + [window makeMainWindow]; + [window makeKeyAndOrderFront:nil]; + + NSMenu *m = [[NSMenu alloc] initWithTitle:@"AMainMenu"]; + NSMenuItem *item = [m addItemWithTitle:@"Apple" action:nil keyEquivalent:@""]; + NSMenu *sm = [[NSMenu alloc] initWithTitle:@"Apple"]; + [m setSubmenu:sm forItem:item]; + [sm addItemWithTitle: @"quit" action:@selector(terminate:) keyEquivalent:@"q"]; + [NSApp setMenu:m]; + [NSApp activateIgnoringOtherApps:YES]; +} + +- (void) applicationDidFinishLaunching:(NSNotification *)notification { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + atexit_b(^{ + // Because activation policy has just been set to behave like a real + // application, that policy must be reset on exit to prevent, among + // other things, the menubar created here from remaining on screen. + [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; + }); + + // Read filename + NSArray *args = [NSProcessInfo processInfo].arguments; + if (args.count < 2) { + NSLog(@"Expected filename on command line"); + exit(1); + } + NSString *filename = args[1]; + + [self createWindow]; + window.pauseButton.target = self; + + mpv = mpv_create(); + if (!mpv) { + printf("failed creating context\n"); + exit(1); + } + + check_error(mpv_set_option_string(mpv, "input-media-keys", "yes")); + // request important errors + check_error(mpv_request_log_messages(mpv, "warn")); + + check_error(mpv_initialize(mpv)); + check_error(mpv_set_option_string(mpv, "vo", "opengl-cb")); + mpv_opengl_cb_context *mpvGL = mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpvGL) { + puts("libmpv does not have the opengl-cb sub-API."); + exit(1); + } + // pass the mpvGL context to our view + window.glView.mpvGL = mpvGL; + int r = mpv_opengl_cb_init_gl(mpvGL, NULL, get_proc_address, NULL); + if (r < 0) { + puts("gl init has failed."); + exit(1); + } + mpv_opengl_cb_set_update_callback(mpvGL, glupdate, (__bridge void *)window.glView); + + // Deal with MPV in the background. + queue = dispatch_queue_create("mpv", DISPATCH_QUEUE_SERIAL); + dispatch_async(queue, ^{ + // Register to be woken up whenever mpv generates new events. + mpv_set_wakeup_callback(mpv, wakeup, (__bridge void *)self); + // Load the indicated file + const char *cmd[] = {"loadfile", filename.UTF8String, NULL}; + check_error(mpv_command(mpv, cmd)); + }); +} + +static void glupdate(void *ctx) +{ + MpvClientOGLView *glView = (__bridge MpvClientOGLView *)ctx; + // I'm still not sure what the best way to handle this is, but this + // works. + dispatch_async(dispatch_get_main_queue(), ^{ + [glView drawRect]; + }); +} + +- (void) handleEvent:(mpv_event *)event +{ + switch (event->event_id) { + case MPV_EVENT_SHUTDOWN: { + mpv_detach_destroy(mpv); + mpv_opengl_cb_uninit_gl(window.glView.mpvGL); + mpv = NULL; + printf("event: shutdown\n"); + break; + } + + case MPV_EVENT_LOG_MESSAGE: { + struct mpv_event_log_message *msg = (struct mpv_event_log_message *)event->data; + printf("[%s] %s: %s", msg->prefix, msg->level, msg->text); + } + + default: + printf("event: %s\n", mpv_event_name(event->event_id)); + } +} + +- (void)togglePause:(NSButton *)button +{ + if (mpv) { + switch (button.state) { + case NSOffState: + { + int pause = 0; + mpv_set_property(mpv, "pause", MPV_FORMAT_FLAG, &pause); + } + break; + case NSOnState: + { + int pause = 1; + mpv_set_property(mpv, "pause", MPV_FORMAT_FLAG, &pause); + } + break; + default: + NSLog(@"This should never happen."); + } + } +} + +- (void) readEvents +{ + dispatch_async(queue, ^{ + while (mpv) { + mpv_event *event = mpv_wait_event(mpv, 0); + if (event->event_id == MPV_EVENT_NONE) + break; + [self handleEvent:event]; + } + }); +} + +static void wakeup(void *context) +{ + AppDelegate *a = (__bridge AppDelegate *) context; + [a readEvents]; +} + +// quit when the window is closed. +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app +{ + return YES; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender +{ + NSLog(@"Terminating."); + const char *args[] = {"quit", NULL}; + mpv_command(mpv, args); + [window.glView clearGLContext]; + return NSTerminateNow; +} + +@end + +// Delete this if you already have a main.m. +int main(int argc, const char * argv[]) { + @autoreleasepool { + NSApplication *app = [NSApplication sharedApplication]; + AppDelegate *delegate = [AppDelegate new]; + app.delegate = delegate; + [app run]; + } + return 0; +} diff --git a/libmpv/cocoa/cocoabasic.m b/libmpv/cocoa/cocoabasic.m new file mode 100644 index 0000000..5a78250 --- /dev/null +++ b/libmpv/cocoa/cocoabasic.m @@ -0,0 +1,209 @@ +// Plays a video from the command line in a view provided by the client +// application. + +// Build with: clang -o cocoabasic cocoabasic.m `pkg-config --libs --cflags mpv` -framework cocoa + +#include + +#include +#include + +static inline void check_error(int status) +{ + if (status < 0) { + printf("mpv API error: %s\n", mpv_error_string(status)); + exit(1); + } +} + +#import + +@interface CocoaWindow : NSWindow +@end + +@implementation CocoaWindow +- (BOOL)canBecomeMainWindow { return YES; } +- (BOOL)canBecomeKeyWindow { return YES; } +@end + +@interface AppDelegate : NSObject +{ + mpv_handle *mpv; + dispatch_queue_t queue; + NSWindow *w; + NSView *wrapper; +} +@end + +static void wakeup(void *); + +@implementation AppDelegate + +- (void)createWindow { + + int mask = NSTitledWindowMask|NSClosableWindowMask| + NSMiniaturizableWindowMask|NSResizableWindowMask; + + self->w = [[CocoaWindow alloc] + initWithContentRect:NSMakeRect(0,0, 1280, 720) + styleMask:mask + backing:NSBackingStoreBuffered + defer:NO]; + + [self->w setTitle:@"cocoabasic example"]; + [self->w makeMainWindow]; + [self->w makeKeyAndOrderFront:nil]; + + NSRect frame = [[self->w contentView] bounds]; + self->wrapper = [[NSView alloc] initWithFrame:frame]; + [self->wrapper setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; + [[self->w contentView] addSubview:self->wrapper]; + [self->wrapper release]; + + NSMenu *m = [[NSMenu alloc] initWithTitle:@"AMainMenu"]; + NSMenuItem *item = [m addItemWithTitle:@"Apple" action:nil keyEquivalent:@""]; + NSMenu *sm = [[NSMenu alloc] initWithTitle:@"Apple"]; + [m setSubmenu:sm forItem:item]; + [sm addItemWithTitle: @"mpv_command('stop')" action:@selector(mpv_stop) keyEquivalent:@""]; + [sm addItemWithTitle: @"mpv_command('quit')" action:@selector(mpv_quit) keyEquivalent:@""]; + [sm addItemWithTitle: @"quit" action:@selector(terminate:) keyEquivalent:@"q"]; + [NSApp setMenu:m]; + [NSApp activateIgnoringOtherApps:YES]; +} + +- (void) applicationDidFinishLaunching:(NSNotification *)notification { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + atexit_b(^{ + // Because activation policy has just been set to behave like a real + // application, that policy must be reset on exit to prevent, among + // other things, the menubar created here from remaining on screen. + [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; + }); + + // Read filename + NSArray *args = [NSProcessInfo processInfo].arguments; + if (args.count < 2) { + NSLog(@"Expected filename on command line"); + exit(1); + } + NSString *filename = args[1]; + + [self createWindow]; + + // Deal with MPV in the background. + queue = dispatch_queue_create("mpv", DISPATCH_QUEUE_SERIAL); + dispatch_async(queue, ^{ + + mpv = mpv_create(); + if (!mpv) { + printf("failed creating context\n"); + exit(1); + } + + int64_t wid = (intptr_t) self->wrapper; + check_error(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid)); + + // Maybe set some options here, like default key bindings. + // NOTE: Interaction with the window seems to be broken for now. + check_error(mpv_set_option_string(mpv, "input-default-bindings", "yes")); + + // for testing! + check_error(mpv_set_option_string(mpv, "input-media-keys", "yes")); + check_error(mpv_set_option_string(mpv, "input-cursor", "no")); + check_error(mpv_set_option_string(mpv, "input-vo-keyboard", "yes")); + + // request important errors + check_error(mpv_request_log_messages(mpv, "warn")); + + check_error(mpv_initialize(mpv)); + + // Register to be woken up whenever mpv generates new events. + mpv_set_wakeup_callback(mpv, wakeup, (__bridge void *) self); + + // Load the indicated file + const char *cmd[] = {"loadfile", filename.UTF8String, NULL}; + check_error(mpv_command(mpv, cmd)); + }); +} + +- (void) handleEvent:(mpv_event *)event +{ + switch (event->event_id) { + case MPV_EVENT_SHUTDOWN: { + mpv_detach_destroy(mpv); + mpv = NULL; + printf("event: shutdown\n"); + break; + } + + case MPV_EVENT_LOG_MESSAGE: { + struct mpv_event_log_message *msg = (struct mpv_event_log_message *)event->data; + printf("[%s] %s: %s", msg->prefix, msg->level, msg->text); + } + + case MPV_EVENT_VIDEO_RECONFIG: { + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *subviews = [self->wrapper subviews]; + if ([subviews count] > 0) { + // mpv's events view + NSView *eview = [self->wrapper subviews][0]; + [self->w makeFirstResponder:eview]; + } + }); + } + + default: + printf("event: %s\n", mpv_event_name(event->event_id)); + } +} + +- (void) readEvents +{ + dispatch_async(queue, ^{ + while (mpv) { + mpv_event *event = mpv_wait_event(mpv, 0); + if (event->event_id == MPV_EVENT_NONE) + break; + [self handleEvent:event]; + } + }); +} + +static void wakeup(void *context) { + AppDelegate *a = (__bridge AppDelegate *) context; + [a readEvents]; +} + +// Ostensibly, mpv's window would be hooked up to this. +- (BOOL) windowShouldClose:(id)sender +{ + return NO; +} + +- (void) mpv_stop +{ + if (mpv) { + const char *args[] = {"stop", NULL}; + mpv_command(mpv, args); + } +} + +- (void) mpv_quit +{ + if (mpv) { + const char *args[] = {"quit", NULL}; + mpv_command(mpv, args); + } +} +@end + +// Delete this if you already have a main.m. +int main(int argc, const char * argv[]) { + @autoreleasepool { + NSApplication *app = [NSApplication sharedApplication]; + AppDelegate *delegate = [AppDelegate new]; + app.delegate = delegate; + [app run]; + } + return 0; +} diff --git a/libmpv/qml/main.cpp b/libmpv/qml/main.cpp new file mode 100644 index 0000000..2122cd8 --- /dev/null +++ b/libmpv/qml/main.cpp @@ -0,0 +1,136 @@ +#include "main.h" + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include + +class MpvRenderer : public QQuickFramebufferObject::Renderer +{ + static void *get_proc_address(void *ctx, const char *name) { + (void)ctx; + QOpenGLContext *glctx = QOpenGLContext::currentContext(); + if (!glctx) + return NULL; + return (void *)glctx->getProcAddress(QByteArray(name)); + } + + mpv::qt::Handle mpv; + QQuickWindow *window; + mpv_opengl_cb_context *mpv_gl; +public: + MpvRenderer(const MpvObject *obj) + : mpv(obj->mpv), window(obj->window()), mpv_gl(obj->mpv_gl) + { + int r = mpv_opengl_cb_init_gl(mpv_gl, NULL, get_proc_address, NULL); + if (r < 0) + throw std::runtime_error("could not initialize OpenGL"); + } + + virtual ~MpvRenderer() + { + // Until this call is done, we need to make sure the player remains + // alive. This is done implicitly with the mpv::qt::Handle instance + // in this class. + mpv_opengl_cb_uninit_gl(mpv_gl); + } + + void render() + { + QOpenGLFramebufferObject *fbo = framebufferObject(); + window->resetOpenGLState(); + mpv_opengl_cb_draw(mpv_gl, fbo->handle(), fbo->width(), fbo->height()); + window->resetOpenGLState(); + } +}; + +MpvObject::MpvObject(QQuickItem * parent) + : QQuickFramebufferObject(parent), mpv_gl(0) +{ + mpv = mpv::qt::Handle::FromRawHandle(mpv_create()); + if (!mpv) + throw std::runtime_error("could not create mpv context"); + + mpv_set_option_string(mpv, "terminal", "yes"); + mpv_set_option_string(mpv, "msg-level", "all=v"); + + if (mpv_initialize(mpv) < 0) + throw std::runtime_error("could not initialize mpv context"); + + // Make use of the MPV_SUB_API_OPENGL_CB API. + mpv::qt::set_option_variant(mpv, "vo", "opengl-cb"); + + // Request hw decoding, just for testing. + mpv::qt::set_option_variant(mpv, "hwdec", "auto"); + + // Setup the callback that will make QtQuick update and redraw if there + // is a new video frame. Use a queued connection: this makes sure the + // doUpdate() function is run on the GUI thread. + mpv_gl = (mpv_opengl_cb_context *)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpv_gl) + throw std::runtime_error("OpenGL not compiled in"); + mpv_opengl_cb_set_update_callback(mpv_gl, MpvObject::on_update, (void *)this); + connect(this, &MpvObject::onUpdate, this, &MpvObject::doUpdate, + Qt::QueuedConnection); +} + +MpvObject::~MpvObject() +{ + if (mpv_gl) + mpv_opengl_cb_set_update_callback(mpv_gl, NULL, NULL); +} + +void MpvObject::on_update(void *ctx) +{ + MpvObject *self = (MpvObject *)ctx; + emit self->onUpdate(); +} + +// connected to onUpdate(); signal makes sure it runs on the GUI thread +void MpvObject::doUpdate() +{ + update(); +} + +void MpvObject::command(const QVariant& params) +{ + mpv::qt::command_variant(mpv, params); +} + +void MpvObject::setProperty(const QString& name, const QVariant& value) +{ + mpv::qt::set_property_variant(mpv, name, value); +} + +QQuickFramebufferObject::Renderer *MpvObject::createRenderer() const +{ + window()->setPersistentOpenGLContext(true); + window()->setPersistentSceneGraph(true); + return new MpvRenderer(this); +} + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + + // Qt sets the locale in the QGuiApplication constructor, but libmpv + // requires the LC_NUMERIC category to be set to "C", so change it back. + std::setlocale(LC_NUMERIC, "C"); + + qmlRegisterType("mpvtest", 1, 0, "MpvObject"); + + QQuickView view; + view.setResizeMode(QQuickView::SizeRootObjectToView); + view.setSource(QUrl("qrc:///mpvtest/main.qml")); + view.show(); + + return app.exec(); +} diff --git a/libmpv/qml/main.h b/libmpv/qml/main.h new file mode 100644 index 0000000..9a65ae5 --- /dev/null +++ b/libmpv/qml/main.h @@ -0,0 +1,36 @@ +#ifndef MPVRENDERER_H_ +#define MPVRENDERER_H_ + +#include + +#include +#include +#include + +class MpvRenderer; + +class MpvObject : public QQuickFramebufferObject +{ + Q_OBJECT + + mpv::qt::Handle mpv; + mpv_opengl_cb_context *mpv_gl; + + friend class MpvRenderer; + +public: + MpvObject(QQuickItem * parent = 0); + virtual ~MpvObject(); + virtual Renderer *createRenderer() const; +public slots: + void command(const QVariant& params); + void setProperty(const QString& name, const QVariant& value); +signals: + void onUpdate(); +private slots: + void doUpdate(); +private: + static void on_update(void *ctx); +}; + +#endif diff --git a/libmpv/qml/main.qml b/libmpv/qml/main.qml new file mode 100644 index 0000000..ec06790 --- /dev/null +++ b/libmpv/qml/main.qml @@ -0,0 +1,71 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0 + +import mpvtest 1.0 + +Item { + width: 1280 + height: 720 + + MpvObject { + id: renderer + anchors.fill: parent + + MouseArea { + anchors.fill: parent + onClicked: renderer.command(["loadfile", "../../../test.mkv"]) + } + } + + Rectangle { + id: labelFrame + anchors.margins: -50 + radius: 5 + color: "white" + border.color: "black" + opacity: 0.8 + anchors.fill: box + } + + Row { + id: box + anchors.bottom: renderer.bottom + anchors.left: renderer.left + anchors.right: renderer.right + anchors.margins: 100 + + Text { + anchors.margins: 10 + wrapMode: Text.WordWrap + text: "QtQuick and mpv are both rendering stuff.\n + Click to load ../../../test.mkv" + } + + // Don't take these controls too seriously. They're for testing. + Column { + CheckBox { + id: checkbox + anchors.margins: 10 + // Heavily filtered means good, right? + text: "Make video look like on a Smart TV" + onClicked: { + if (checkbox.checked) { + renderer.command(["vo_cmdline", "sharpen=5.0"]) + } else { + renderer.command(["vo_cmdline", ""]) + } + } + } + Slider { + id: slider + anchors.margins: 10 + anchors.left: checkbox.left + anchors.right: checkbox.right + minimumValue: -100 + maximumValue: 100 + value: 0 + onValueChanged: renderer.setProperty("gamma", slider.value | 0) + } + } + } +} diff --git a/libmpv/qml/mpvtest.pro b/libmpv/qml/mpvtest.pro new file mode 100644 index 0000000..0b2e96f --- /dev/null +++ b/libmpv/qml/mpvtest.pro @@ -0,0 +1,12 @@ +QT += qml quick + +HEADERS += main.h +SOURCES += main.cpp + +QT_CONFIG -= no-pkg-config +CONFIG += link_pkgconfig debug +PKGCONFIG += mpv + +RESOURCES += mpvtest.qrc + +OTHER_FILES += main.qml diff --git a/libmpv/qml/mpvtest.qrc b/libmpv/qml/mpvtest.qrc new file mode 100644 index 0000000..bb67265 --- /dev/null +++ b/libmpv/qml/mpvtest.qrc @@ -0,0 +1,5 @@ + + + main.qml + + diff --git a/libmpv/qml_direct/main.cpp b/libmpv/qml_direct/main.cpp new file mode 100644 index 0000000..37c6119 --- /dev/null +++ b/libmpv/qml_direct/main.cpp @@ -0,0 +1,159 @@ +#include "main.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +static void *get_proc_address(void *ctx, const char *name) { + (void)ctx; + QOpenGLContext *glctx = QOpenGLContext::currentContext(); + if (!glctx) + return NULL; + return (void *)glctx->getProcAddress(QByteArray(name)); +} + +MpvRenderer::MpvRenderer(mpv::qt::Handle a_mpv, mpv_opengl_cb_context *a_mpv_gl) + : mpv(a_mpv), mpv_gl(a_mpv_gl), window(0), size() +{ + int r = mpv_opengl_cb_init_gl(mpv_gl, NULL, get_proc_address, NULL); + if (r < 0) + throw std::runtime_error("could not initialize OpenGL"); +} + +MpvRenderer::~MpvRenderer() +{ + // Until this call is done, we need to make sure the player remains + // alive. This is done implicitly with the mpv::qt::Handle instance + // in this class. + mpv_opengl_cb_uninit_gl(mpv_gl); +} + +void MpvRenderer::paint() +{ + window->resetOpenGLState(); + + // This uses 0 as framebuffer, which indicates that mpv will render directly + // to the frontbuffer. Note that mpv will always switch framebuffers + // explicitly. Some QWindow setups (such as using QQuickWidget) actually + // want you to render into a FBO in the beforeRendering() signal, and this + // code won't work there. + // The negation is used for rendering with OpenGL's flipped coordinates. + mpv_opengl_cb_draw(mpv_gl, 0, size.width(), -size.height()); + + window->resetOpenGLState(); +} + +MpvObject::MpvObject(QQuickItem * parent) + : QQuickItem(parent), mpv_gl(0), renderer(0) +{ + mpv = mpv::qt::Handle::FromRawHandle(mpv_create()); + if (!mpv) + throw std::runtime_error("could not create mpv context"); + + mpv_set_option_string(mpv, "terminal", "yes"); + mpv_set_option_string(mpv, "msg-level", "all=v"); + + if (mpv_initialize(mpv) < 0) + throw std::runtime_error("could not initialize mpv context"); + + // Make use of the MPV_SUB_API_OPENGL_CB API. + mpv::qt::set_option_variant(mpv, "vo", "opengl-cb"); + + // Setup the callback that will make QtQuick update and redraw if there + // is a new video frame. Use a queued connection: this makes sure the + // doUpdate() function is run on the GUI thread. + mpv_gl = (mpv_opengl_cb_context *)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpv_gl) + throw std::runtime_error("OpenGL not compiled in"); + mpv_opengl_cb_set_update_callback(mpv_gl, MpvObject::on_update, (void *)this); + connect(this, &MpvObject::onUpdate, this, &MpvObject::doUpdate, + Qt::QueuedConnection); + + connect(this, &QQuickItem::windowChanged, + this, &MpvObject::handleWindowChanged); +} + +MpvObject::~MpvObject() +{ + if (mpv_gl) + mpv_opengl_cb_set_update_callback(mpv_gl, NULL, NULL); +} + +void MpvObject::handleWindowChanged(QQuickWindow *win) +{ + if (!win) + return; + connect(win, &QQuickWindow::beforeSynchronizing, + this, &MpvObject::sync, Qt::DirectConnection); + connect(win, &QQuickWindow::sceneGraphInvalidated, + this, &MpvObject::cleanup, Qt::DirectConnection); + connect(win, &QQuickWindow::frameSwapped, + this, &MpvObject::swapped, Qt::DirectConnection); + win->setClearBeforeRendering(false); +} + +void MpvObject::sync() +{ + if (!renderer) { + renderer = new MpvRenderer(mpv, mpv_gl); + connect(window(), &QQuickWindow::beforeRendering, + renderer, &MpvRenderer::paint, Qt::DirectConnection); + } + renderer->window = window(); + renderer->size = window()->size() * window()->devicePixelRatio(); +} + +void MpvObject::swapped() +{ + mpv_opengl_cb_report_flip(mpv_gl, 0); +} + +void MpvObject::cleanup() +{ + if (renderer) { + delete renderer; + renderer = 0; + } +} + +void MpvObject::on_update(void *ctx) +{ + MpvObject *self = (MpvObject *)ctx; + emit self->onUpdate(); +} + +// connected to onUpdate(); signal makes sure it runs on the GUI thread +void MpvObject::doUpdate() +{ + window()->update(); +} + +void MpvObject::command(const QVariant& params) +{ + mpv::qt::command_variant(mpv, params); +} + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + + // Qt sets the locale in the QGuiApplication constructor, but libmpv + // requires the LC_NUMERIC category to be set to "C", so change it back. + std::setlocale(LC_NUMERIC, "C"); + + qmlRegisterType("mpvtest", 1, 0, "MpvObject"); + + QQuickView view; + view.setResizeMode(QQuickView::SizeRootObjectToView); + view.setSource(QUrl("qrc:///mpvtest/main.qml")); + view.show(); + + return app.exec(); +} diff --git a/libmpv/qml_direct/main.h b/libmpv/qml_direct/main.h new file mode 100644 index 0000000..b0310ff --- /dev/null +++ b/libmpv/qml_direct/main.h @@ -0,0 +1,51 @@ +#ifndef MPVRENDERER_H_ +#define MPVRENDERER_H_ + +#include + +#include +#include +#include + +class MpvRenderer : public QObject +{ + Q_OBJECT + mpv::qt::Handle mpv; + mpv_opengl_cb_context *mpv_gl; + QQuickWindow *window; + QSize size; + + friend class MpvObject; +public: + MpvRenderer(mpv::qt::Handle a_mpv, mpv_opengl_cb_context *a_mpv_gl); + virtual ~MpvRenderer(); +public slots: + void paint(); +}; + +class MpvObject : public QQuickItem +{ + Q_OBJECT + + mpv::qt::Handle mpv; + mpv_opengl_cb_context *mpv_gl; + MpvRenderer *renderer; + +public: + MpvObject(QQuickItem * parent = 0); + virtual ~MpvObject(); +public slots: + void command(const QVariant& params); + void sync(); + void swapped(); + void cleanup(); +signals: + void onUpdate(); +private slots: + void doUpdate(); + void handleWindowChanged(QQuickWindow *win); +private: + static void on_update(void *ctx); +}; + +#endif diff --git a/libmpv/qml_direct/main.qml b/libmpv/qml_direct/main.qml new file mode 100644 index 0000000..92be9bc --- /dev/null +++ b/libmpv/qml_direct/main.qml @@ -0,0 +1,49 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0 + +import mpvtest 1.0 + +Item { + width: 1280 + height: 720 + + MpvObject { + id: renderer + + // This object isn't real and not visible; it just renders into the + // background of the containing Window. + width: 0 + height: 0 + } + + MouseArea { + anchors.fill: parent + onClicked: renderer.command(["loadfile", "../../../test.mkv"]) + } + + Rectangle { + id: labelFrame + anchors.margins: -50 + radius: 5 + color: "white" + border.color: "black" + opacity: 0.8 + anchors.fill: box + } + + Row { + id: box + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 100 + + Text { + anchors.margins: 10 + wrapMode: Text.WordWrap + text: "QtQuick and mpv are both rendering stuff.\n + In this example, mpv is always in the background.\n + Click to load ../../../test.mkv" + } + } +} diff --git a/libmpv/qml_direct/mpvtest.pro b/libmpv/qml_direct/mpvtest.pro new file mode 100644 index 0000000..0b2e96f --- /dev/null +++ b/libmpv/qml_direct/mpvtest.pro @@ -0,0 +1,12 @@ +QT += qml quick + +HEADERS += main.h +SOURCES += main.cpp + +QT_CONFIG -= no-pkg-config +CONFIG += link_pkgconfig debug +PKGCONFIG += mpv + +RESOURCES += mpvtest.qrc + +OTHER_FILES += main.qml diff --git a/libmpv/qml_direct/mpvtest.qrc b/libmpv/qml_direct/mpvtest.qrc new file mode 100644 index 0000000..bb67265 --- /dev/null +++ b/libmpv/qml_direct/mpvtest.qrc @@ -0,0 +1,5 @@ + + + main.qml + + diff --git a/libmpv/qt/qtexample.cpp b/libmpv/qt/qtexample.cpp new file mode 100644 index 0000000..5f4e86e --- /dev/null +++ b/libmpv/qt/qtexample.cpp @@ -0,0 +1,225 @@ +// This example can be built with: qmake && make + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION >= 0x050000 +#include +#endif + +#include + +#include "qtexample.h" + +static void wakeup(void *ctx) +{ + // This callback is invoked from any mpv thread (but possibly also + // recursively from a thread that is calling the mpv API). Just notify + // the Qt GUI thread to wake up (so that it can process events with + // mpv_wait_event()), and return as quickly as possible. + MainWindow *mainwindow = (MainWindow *)ctx; + emit mainwindow->mpv_events(); +} + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent) +{ + setWindowTitle("Qt embedding demo"); + setMinimumSize(640, 480); + + QMenu *menu = menuBar()->addMenu(tr("&File")); + QAction *on_open = new QAction(tr("&Open"), this); + on_open->setShortcuts(QKeySequence::Open); + on_open->setStatusTip(tr("Open a file")); + connect(on_open, &QAction::triggered, this, &MainWindow::on_file_open); + menu->addAction(on_open); + + QAction *on_new = new QAction(tr("&New window"), this); + connect(on_new, &QAction::triggered, this, &MainWindow::on_new_window); + menu->addAction(on_new); + + statusBar(); + + QMainWindow *log_window = new QMainWindow(this); + log = new QTextEdit(log_window); + log->setReadOnly(true); + log_window->setCentralWidget(log); + log_window->setWindowTitle("mpv log window"); + log_window->setMinimumSize(500, 50); + log_window->show(); + + mpv = mpv_create(); + if (!mpv) + throw std::runtime_error("can't create mpv instance"); + + // Create a video child window. Force Qt to create a native window, and + // pass the window ID to the mpv wid option. Works on: X11, win32, Cocoa + mpv_container = new QWidget(this); + setCentralWidget(mpv_container); + mpv_container->setAttribute(Qt::WA_DontCreateNativeAncestors); + mpv_container->setAttribute(Qt::WA_NativeWindow); + // If you have a HWND, use: int64_t wid = (intptr_t)hwnd; + int64_t wid = mpv_container->winId(); + mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid); + + // Enable default bindings, because we're lazy. Normally, a player using + // mpv as backend would implement its own key bindings. + mpv_set_option_string(mpv, "input-default-bindings", "yes"); + + // Enable keyboard input on the X11 window. For the messy details, see + // --input-vo-keyboard on the manpage. + mpv_set_option_string(mpv, "input-vo-keyboard", "yes"); + + // Let us receive property change events with MPV_EVENT_PROPERTY_CHANGE if + // this property changes. + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + + mpv_observe_property(mpv, 0, "track-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "chapter-list", MPV_FORMAT_NODE); + + // Request log messages with level "info" or higher. + // They are received as MPV_EVENT_LOG_MESSAGE. + mpv_request_log_messages(mpv, "info"); + + // From this point on, the wakeup function will be called. The callback + // can come from any thread, so we use the QueuedConnection mechanism to + // relay the wakeup in a thread-safe way. + connect(this, &MainWindow::mpv_events, this, &MainWindow::on_mpv_events, + Qt::QueuedConnection); + mpv_set_wakeup_callback(mpv, wakeup, this); + + if (mpv_initialize(mpv) < 0) + throw std::runtime_error("mpv failed to initialize"); +} + +void MainWindow::handle_mpv_event(mpv_event *event) +{ + switch (event->event_id) { + case MPV_EVENT_PROPERTY_CHANGE: { + mpv_event_property *prop = (mpv_event_property *)event->data; + if (strcmp(prop->name, "time-pos") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double time = *(double *)prop->data; + std::stringstream ss; + ss << "At: " << time; + statusBar()->showMessage(QString::fromStdString(ss.str())); + } else if (prop->format == MPV_FORMAT_NONE) { + // The property is unavailable, which probably means playback + // was stopped. + statusBar()->showMessage(""); + } + } else if (strcmp(prop->name, "chapter-list") == 0 || + strcmp(prop->name, "track-list") == 0) + { + // Dump the properties as JSON for demo purposes. +#if QT_VERSION >= 0x050000 + if (prop->format == MPV_FORMAT_NODE) { + QVariant v = mpv::qt::node_to_variant((mpv_node *)prop->data); + // Abuse JSON support for easily printing the mpv_node contents. + QJsonDocument d = QJsonDocument::fromVariant(v); + append_log("Change property " + QString(prop->name) + ":\n"); + append_log(d.toJson().data()); + } +#endif + } + break; + } + case MPV_EVENT_VIDEO_RECONFIG: { + // Retrieve the new video size. + int64_t w, h; + if (mpv_get_property(mpv, "dwidth", MPV_FORMAT_INT64, &w) >= 0 && + mpv_get_property(mpv, "dheight", MPV_FORMAT_INT64, &h) >= 0 && + w > 0 && h > 0) + { + // Note that the MPV_EVENT_VIDEO_RECONFIG event doesn't necessarily + // imply a resize, and you should check yourself if the video + // dimensions really changed. + // mpv itself will scale/letter box the video to the container size + // if the video doesn't fit. + std::stringstream ss; + ss << "Reconfig: " << w << " " << h; + statusBar()->showMessage(QString::fromStdString(ss.str())); + } + break; + } + case MPV_EVENT_LOG_MESSAGE: { + struct mpv_event_log_message *msg = (struct mpv_event_log_message *)event->data; + std::stringstream ss; + ss << "[" << msg->prefix << "] " << msg->level << ": " << msg->text; + append_log(QString::fromStdString(ss.str())); + break; + } + case MPV_EVENT_SHUTDOWN: { + mpv_terminate_destroy(mpv); + mpv = NULL; + break; + } + default: ; + // Ignore uninteresting or unknown events. + } +} + +// This slot is invoked by wakeup() (through the mpv_events signal). +void MainWindow::on_mpv_events() +{ + // Process all events, until the event queue is empty. + while (mpv) { + mpv_event *event = mpv_wait_event(mpv, 0); + if (event->event_id == MPV_EVENT_NONE) + break; + handle_mpv_event(event); + } +} + +void MainWindow::on_file_open() +{ + QString filename = QFileDialog::getOpenFileName(this, "Open file"); + if (mpv) { + const QByteArray c_filename = filename.toUtf8(); + const char *args[] = {"loadfile", c_filename.data(), NULL}; + mpv_command_async(mpv, 0, args); + } +} + +void MainWindow::on_new_window() +{ + (new MainWindow())->show(); +} + +void MainWindow::append_log(const QString &text) +{ + QTextCursor cursor = log->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(text); + log->setTextCursor(cursor); +} + +MainWindow::~MainWindow() +{ + if (mpv) + mpv_terminate_destroy(mpv); +} + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + // Qt sets the locale in the QApplication constructor, but libmpv requires + // the LC_NUMERIC category to be set to "C", so change it back. + std::setlocale(LC_NUMERIC, "C"); + + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/libmpv/qt/qtexample.h b/libmpv/qt/qtexample.h new file mode 100644 index 0000000..c3891cf --- /dev/null +++ b/libmpv/qt/qtexample.h @@ -0,0 +1,37 @@ +#ifndef QTEXAMPLE_H +#define QTEXAMPLE_H + +#include + +#include + +class QTextEdit; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void on_file_open(); + void on_new_window(); + void on_mpv_events(); + +signals: + void mpv_events(); + +private: + QWidget *mpv_container; + mpv_handle *mpv; + QTextEdit *log; + + void append_log(const QString &text); + + void create_player(); + void handle_mpv_event(mpv_event *event); +}; + +#endif // QTEXAMPLE_H diff --git a/libmpv/qt/qtexample.pro b/libmpv/qt/qtexample.pro new file mode 100644 index 0000000..7c0a6ad --- /dev/null +++ b/libmpv/qt/qtexample.pro @@ -0,0 +1,13 @@ +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = qtexample +TEMPLATE = app + +QT_CONFIG -= no-pkg-config +CONFIG += link_pkgconfig debug +PKGCONFIG += mpv + +SOURCES += qtexample.cpp +HEADERS += qtexample.h diff --git a/libmpv/qt_opengl/main.cpp b/libmpv/qt_opengl/main.cpp new file mode 100644 index 0000000..086a4b4 --- /dev/null +++ b/libmpv/qt_opengl/main.cpp @@ -0,0 +1,13 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + // Qt sets the locale in the QApplication constructor, but libmpv requires + // the LC_NUMERIC category to be set to "C", so change it back. + setlocale(LC_NUMERIC, "C"); + MainWindow w; + w.show(); + return a.exec(); +} diff --git a/libmpv/qt_opengl/mainwindow.cpp b/libmpv/qt_opengl/mainwindow.cpp new file mode 100644 index 0000000..3ecd0a1 --- /dev/null +++ b/libmpv/qt_opengl/mainwindow.cpp @@ -0,0 +1,52 @@ +#include "mainwindow.h" +#include "mpvwidget.h" +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) : QWidget(parent) +{ + m_mpv = new MpvWidget(this); + m_slider = new QSlider(); + m_slider->setOrientation(Qt::Horizontal); + m_openBtn = new QPushButton("Open"); + m_playBtn = new QPushButton("Pause"); + QHBoxLayout *hb = new QHBoxLayout(); + hb->addWidget(m_openBtn); + hb->addWidget(m_playBtn); + QVBoxLayout *vl = new QVBoxLayout(); + vl->addWidget(m_mpv); + vl->addWidget(m_slider); + vl->addLayout(hb); + setLayout(vl); + connect(m_slider, SIGNAL(sliderMoved(int)), SLOT(seek(int))); + connect(m_openBtn, SIGNAL(clicked()), SLOT(openMedia())); + connect(m_playBtn, SIGNAL(clicked()), SLOT(pauseResume())); + connect(m_mpv, SIGNAL(positionChanged(int)), m_slider, SLOT(setValue(int))); + connect(m_mpv, SIGNAL(durationChanged(int)), this, SLOT(setSliderRange(int))); +} + +void MainWindow::openMedia() +{ + QString file = QFileDialog::getOpenFileName(0, "Open a video"); + if (file.isEmpty()) + return; + m_mpv->command(QStringList() << "loadfile" << file); +} + +void MainWindow::seek(int pos) +{ + m_mpv->command(QVariantList() << "seek" << pos << "absolute"); +} + +void MainWindow::pauseResume() +{ + const bool paused = m_mpv->getProperty("pause").toBool(); + m_mpv->setProperty("pause", !paused); +} + +void MainWindow::setSliderRange(int duration) +{ + m_slider->setRange(0, duration); +} diff --git a/libmpv/qt_opengl/mainwindow.h b/libmpv/qt_opengl/mainwindow.h new file mode 100644 index 0000000..523b385 --- /dev/null +++ b/libmpv/qt_opengl/mainwindow.h @@ -0,0 +1,27 @@ +#ifndef MainWindow_H +#define MainWindow_H + +#include + +class MpvWidget; +class QSlider; +class QPushButton; +class MainWindow : public QWidget +{ + Q_OBJECT +public: + explicit MainWindow(QWidget *parent = 0); +public Q_SLOTS: + void openMedia(); + void seek(int pos); + void pauseResume(); +private Q_SLOTS: + void setSliderRange(int duration); +private: + MpvWidget *m_mpv; + QSlider *m_slider; + QPushButton *m_openBtn; + QPushButton *m_playBtn; +}; + +#endif // MainWindow_H diff --git a/libmpv/qt_opengl/mpvwidget.cpp b/libmpv/qt_opengl/mpvwidget.cpp new file mode 100644 index 0000000..4d2cad6 --- /dev/null +++ b/libmpv/qt_opengl/mpvwidget.cpp @@ -0,0 +1,129 @@ +#include "mpvwidget.h" +#include +#include +#include + +static void wakeup(void *ctx) +{ + QMetaObject::invokeMethod((MpvWidget*)ctx, "on_mpv_events", Qt::QueuedConnection); +} + +static void *get_proc_address(void *ctx, const char *name) { + Q_UNUSED(ctx); + QOpenGLContext *glctx = QOpenGLContext::currentContext(); + if (!glctx) + return NULL; + return (void *)glctx->getProcAddress(QByteArray(name)); +} + +MpvWidget::MpvWidget(QWidget *parent, Qt::WindowFlags f) + : QOpenGLWidget(parent, f) +{ + mpv = mpv::qt::Handle::FromRawHandle(mpv_create()); + if (!mpv) + throw std::runtime_error("could not create mpv context"); + + mpv_set_option_string(mpv, "terminal", "yes"); + mpv_set_option_string(mpv, "msg-level", "all=v"); + if (mpv_initialize(mpv) < 0) + throw std::runtime_error("could not initialize mpv context"); + + // Make use of the MPV_SUB_API_OPENGL_CB API. + mpv::qt::set_option_variant(mpv, "vo", "opengl-cb"); + + // Request hw decoding, just for testing. + mpv::qt::set_option_variant(mpv, "hwdec", "auto"); + + mpv_gl = (mpv_opengl_cb_context *)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpv_gl) + throw std::runtime_error("OpenGL not compiled in"); + mpv_opengl_cb_set_update_callback(mpv_gl, MpvWidget::on_update, (void *)this); + connect(this, SIGNAL(frameSwapped()), SLOT(swapped())); + + mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + mpv_set_wakeup_callback(mpv, wakeup, this); +} + +MpvWidget::~MpvWidget() +{ + makeCurrent(); + if (mpv_gl) + mpv_opengl_cb_set_update_callback(mpv_gl, NULL, NULL); + // Until this call is done, we need to make sure the player remains + // alive. This is done implicitly with the mpv::qt::Handle instance + // in this class. + mpv_opengl_cb_uninit_gl(mpv_gl); +} + +void MpvWidget::command(const QVariant& params) +{ + mpv::qt::command_variant(mpv, params); +} + +void MpvWidget::setProperty(const QString& name, const QVariant& value) +{ + mpv::qt::set_property_variant(mpv, name, value); +} + +QVariant MpvWidget::getProperty(const QString &name) const +{ + return mpv::qt::get_property_variant(mpv, name); +} + +void MpvWidget::initializeGL() +{ + int r = mpv_opengl_cb_init_gl(mpv_gl, NULL, get_proc_address, NULL); + if (r < 0) + throw std::runtime_error("could not initialize OpenGL"); +} + +void MpvWidget::paintGL() +{ + mpv_opengl_cb_draw(mpv_gl, defaultFramebufferObject(), width(), -height()); +} + +void MpvWidget::swapped() +{ + mpv_opengl_cb_report_flip(mpv_gl, 0); +} + +void MpvWidget::on_mpv_events() +{ + // Process all events, until the event queue is empty. + while (mpv) { + mpv_event *event = mpv_wait_event(mpv, 0); + if (event->event_id == MPV_EVENT_NONE) { + break; + } + handle_mpv_event(event); + } +} + +void MpvWidget::handle_mpv_event(mpv_event *event) +{ + switch (event->event_id) { + case MPV_EVENT_PROPERTY_CHANGE: { + mpv_event_property *prop = (mpv_event_property *)event->data; + if (strcmp(prop->name, "time-pos") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double time = *(double *)prop->data; + Q_EMIT positionChanged(time); + } + } else if (strcmp(prop->name, "duration") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double time = *(double *)prop->data; + Q_EMIT durationChanged(time); + } + } + break; + } + default: ; + // Ignore uninteresting or unknown events. + } +} + +void MpvWidget::on_update(void *ctx) +{ + QMetaObject::invokeMethod((MpvWidget*)ctx, "update"); +} diff --git a/libmpv/qt_opengl/mpvwidget.h b/libmpv/qt_opengl/mpvwidget.h new file mode 100644 index 0000000..a219681 --- /dev/null +++ b/libmpv/qt_opengl/mpvwidget.h @@ -0,0 +1,38 @@ +#ifndef PLAYERWINDOW_H +#define PLAYERWINDOW_H + +#include +#include +#include +#include + +class MpvWidget Q_DECL_FINAL: public QOpenGLWidget +{ + Q_OBJECT +public: + MpvWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + ~MpvWidget(); + void command(const QVariant& params); + void setProperty(const QString& name, const QVariant& value); + QVariant getProperty(const QString& name) const; + QSize sizeHint() const { return QSize(480, 270);} +Q_SIGNALS: + void durationChanged(int value); + void positionChanged(int value); +protected: + void initializeGL() Q_DECL_OVERRIDE; + void paintGL() Q_DECL_OVERRIDE; +private Q_SLOTS: + void swapped(); + void on_mpv_events(); +private: + void handle_mpv_event(mpv_event *event); + static void on_update(void *ctx); + + mpv::qt::Handle mpv; + mpv_opengl_cb_context *mpv_gl; +}; + + + +#endif // PLAYERWINDOW_H diff --git a/libmpv/qt_opengl/qt_opengl.pro b/libmpv/qt_opengl/qt_opengl.pro new file mode 100644 index 0000000..8a2bcc6 --- /dev/null +++ b/libmpv/qt_opengl/qt_opengl.pro @@ -0,0 +1,13 @@ +CONFIG -= app_bundle +QT += widgets + +QT_CONFIG -= no-pkg-config +CONFIG += link_pkgconfig debug +PKGCONFIG += mpv + +HEADERS = \ + mpvwidget.h \ + mainwindow.h +SOURCES = main.cpp \ + mpvwidget.cpp \ + mainwindow.cpp diff --git a/libmpv/sdl/main.c b/libmpv/sdl/main.c new file mode 100644 index 0000000..b3b5952 --- /dev/null +++ b/libmpv/sdl/main.c @@ -0,0 +1,158 @@ +// Build with: gcc -o main main.c `pkg-config --libs --cflags mpv sdl2` + +#include +#include +#include + +#include + +#include +#include + +static Uint32 wakeup_on_mpv_redraw, wakeup_on_mpv_events; + +static void die(const char *msg) +{ + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void *get_proc_address_mpv(void *fn_ctx, const char *name) +{ + return SDL_GL_GetProcAddress(name); +} + +static void on_mpv_events(void *ctx) +{ + SDL_Event event = {.type = wakeup_on_mpv_events}; + SDL_PushEvent(&event); +} + +static void on_mpv_redraw(void *ctx) +{ + SDL_Event event = {.type = wakeup_on_mpv_redraw}; + SDL_PushEvent(&event); +} + +int main(int argc, char *argv[]) +{ + if (argc != 2) + die("pass a single media file as argument"); + + mpv_handle *mpv = mpv_create(); + if (!mpv) + die("context init failed"); + + // Some minor options can only be set before mpv_initialize(). + if (mpv_initialize(mpv) < 0) + die("mpv init failed"); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) + die("SDL init failed"); + + SDL_Window *window = + SDL_CreateWindow("hi", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 1000, 500, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | + SDL_WINDOW_RESIZABLE); + if (!window) + die("failed to create SDL window"); + + // The OpenGL API is somewhat separate from the normal mpv API. This only + // returns NULL if no OpenGL support is compiled. + mpv_opengl_cb_context *mpv_gl = mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpv_gl) + die("failed to create mpv GL API handle"); + + SDL_GLContext glcontext = SDL_GL_CreateContext(window); + if (!glcontext) + die("failed to create SDL GL context"); + + // This makes mpv use the currently set GL context. It will use the callback + // to resolve GL builtin functions, as well as extensions. + if (mpv_opengl_cb_init_gl(mpv_gl, NULL, get_proc_address_mpv, NULL) < 0) + die("failed to initialize mpv GL context"); + + // Actually using the opengl_cb state has to be explicitly requested. + // Otherwise, mpv will create a separate platform window. + if (mpv_set_option_string(mpv, "vo", "opengl-cb") < 0) + die("failed to set VO"); + + // We use events for thread-safe notification of the SDL main loop. + // Generally, the wakeup callbacks (set further below) should do as least + // work as possible, and merely wake up another thread to do actual work. + // On SDL, waking up the mainloop is the ideal course of action. SDL's + // SDL_PushEvent() is thread-safe, so we use that. + wakeup_on_mpv_redraw = SDL_RegisterEvents(1); + wakeup_on_mpv_events = SDL_RegisterEvents(1); + if (wakeup_on_mpv_redraw == (Uint32)-1 || wakeup_on_mpv_events == (Uint32)-1) + die("could not register events"); + + // When normal mpv events are available. + mpv_set_wakeup_callback(mpv, on_mpv_events, NULL); + + // When a new frame should be drawn with mpv_opengl_cb_draw(). + // (Separate from the normal event handling mechanism for the sake of + // users which run OpenGL on a different thread.) + mpv_opengl_cb_set_update_callback(mpv_gl, on_mpv_redraw, NULL); + + // Play this file. Note that this starts playback asynchronously. + const char *cmd[] = {"loadfile", argv[1], NULL}; + mpv_command(mpv, cmd); + + while (1) { + SDL_Event event; + if (SDL_WaitEvent(&event) != 1) + die("event loop error"); + int redraw = 0; + switch (event.type) { + case SDL_QUIT: + goto done; + case SDL_WINDOWEVENT: + if (event.window.event == SDL_WINDOWEVENT_EXPOSED) + redraw = 1; + break; + case SDL_KEYDOWN: + if (event.key.keysym.sym == SDLK_SPACE) + mpv_command_string(mpv, "cycle pause"); + break; + default: + // Happens when a new video frame should be rendered, or if the + // current frame has to be redrawn e.g. due to OSD changes. + if (event.type == wakeup_on_mpv_redraw) + redraw = 1; + // Happens when at least 1 new event is in the mpv event queue. + if (event.type == wakeup_on_mpv_events) { + // Handle all remaining mpv events. + while (1) { + mpv_event *mp_event = mpv_wait_event(mpv, 0); + if (mp_event->event_id == MPV_EVENT_NONE) + break; + printf("event: %s\n", mpv_event_name(mp_event->event_id)); + } + } + } + if (redraw) { + int w, h; + SDL_GetWindowSize(window, &w, &h); + // Note: + // - The 0 is the FBO to use; 0 is the default framebuffer (i.e. + // render to the window directly. + // - The negative height tells mpv to flip the coordinate system. + // - If you do not want the video to cover the whole screen, or want + // to apply any form of fancy transformation, you will have to + // render to a FBO. + // - See opengl_cb.h on what OpenGL environment mpv expects, and + // other API details. + mpv_opengl_cb_draw(mpv_gl, 0, w, -h); + SDL_GL_SwapWindow(window); + } + } +done: + + // Destroy the GL renderer and all of the GL objects it allocated. If video + // is still running, the video track will be deselected. + mpv_opengl_cb_uninit_gl(mpv_gl); + + mpv_terminate_destroy(mpv); + return 0; +} diff --git a/libmpv/simple/simple.c b/libmpv/simple/simple.c new file mode 100644 index 0000000..3ba1c35 --- /dev/null +++ b/libmpv/simple/simple.c @@ -0,0 +1,54 @@ +// Build with: gcc -o simple simple.c `pkg-config --libs --cflags mpv` + +#include +#include +#include + +#include + +static inline void check_error(int status) +{ + if (status < 0) { + printf("mpv API error: %s\n", mpv_error_string(status)); + exit(1); + } +} + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + printf("pass a single media file as argument\n"); + return 1; + } + + mpv_handle *ctx = mpv_create(); + if (!ctx) { + printf("failed creating context\n"); + return 1; + } + + // Enable default key bindings, so the user can actually interact with + // the player (and e.g. close the window). + check_error(mpv_set_option_string(ctx, "input-default-bindings", "yes")); + mpv_set_option_string(ctx, "input-vo-keyboard", "yes"); + int val = 1; + check_error(mpv_set_option(ctx, "osc", MPV_FORMAT_FLAG, &val)); + + // Done setting up options. + check_error(mpv_initialize(ctx)); + + // Play this file. + const char *cmd[] = {"loadfile", argv[1], NULL}; + check_error(mpv_command(ctx, cmd)); + + // Let it play, and wait until the user quit