├── .gitignore ├── Source ├── MPlayerShell-Prefix.pch ├── VideoLayer.h ├── MediaView.h ├── AppController.h ├── MPlayerOSXVOProto.h ├── main.m ├── VideoRenderer.h ├── PresentationController.h ├── MediaView.m ├── VideoLayer.m ├── mps.1 ├── VideoRenderer.m ├── AppController.m └── PresentationController.m ├── MPlayerShell.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Makefile ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | xcuserdata/ 4 | -------------------------------------------------------------------------------- /Source/MPlayerShell-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // MPlayerShell-Prefix.pch 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #ifdef __OBJC__ 9 | #import 10 | #endif 11 | -------------------------------------------------------------------------------- /MPlayerShell.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Source/VideoLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoLayer.h 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | 10 | @interface VideoLayer : CAOpenGLLayer 11 | 12 | @property CVPixelBufferRef pixelBuffer; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Source/MediaView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaView.h 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | #import "VideoLayer.h" 10 | 11 | @interface MediaView : NSView 12 | 13 | @property NSSize displaySize; 14 | @property (readonly) VideoLayer *videoLayer; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Source/AppController.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppController.h 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | #import "PresentationController.h" 10 | 11 | @interface AppController : NSObject 12 | 13 | - (void)run; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Source/MPlayerOSXVOProto.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPlayerOSXVOProto.h 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | @protocol MPlayerOSXVOProto 9 | - (int)startWithWidth:(bycopy int)width 10 | withHeight:(bycopy int)height 11 | withBytes:(bycopy int)bytes 12 | withAspect:(bycopy int)aspect; 13 | - (void)stop; 14 | - (void)render; 15 | - (void)toggleFullscreen; 16 | - (void)ontop; 17 | @end 18 | -------------------------------------------------------------------------------- /Source/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import "AppController.h" 9 | 10 | int main() 11 | { 12 | if (NSAppKitVersionNumber < NSAppKitVersionNumber10_7) { 13 | NSLog(@"OS X Lion (version 10.7) or later required"); 14 | exit(EXIT_FAILURE); 15 | } 16 | @autoreleasepool { 17 | [[AppController new] run]; 18 | } 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /Source/VideoRenderer.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoRenderer.h 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | #import "MPlayerOSXVOProto.h" 10 | #import "VideoLayer.h" 11 | 12 | @protocol VideoRendererDelegate; 13 | 14 | @interface VideoRenderer : NSObject 15 | 16 | - (id)initWithDelegate:(id )aDelegate 17 | sharedBufferName:(NSString *)aName 18 | videoLayer:(VideoLayer *)aLayer; 19 | 20 | @end 21 | 22 | @protocol VideoRendererDelegate 23 | - (void)startRenderingWithDisplaySize:(NSSize)aSize; 24 | - (void)stopRendering; 25 | @end 26 | -------------------------------------------------------------------------------- /Source/PresentationController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationController.h 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | #import "VideoRenderer.h" 10 | #import "MediaView.h" 11 | 12 | @protocol PresentationControllerDelegate; 13 | 14 | @interface PresentationController : NSObject 15 | 16 | - (id)initWithDelegate:(id )aDelegate 17 | appName:(NSString *)aString 18 | fullScreenMode:(BOOL)fullScreenFlag 19 | floatOnTopMode:(BOOL)floatOnTopFlag; 20 | 21 | @property (readonly) MediaView *mediaView; 22 | 23 | @end 24 | 25 | @protocol PresentationControllerDelegate 26 | - (void)sendCommand:(NSString *)command; 27 | @end 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRCDIR := ./Source/ 2 | SRCFILES := main.m AppController.m MediaView.m PresentationController.m VideoLayer.m VideoRenderer.m 3 | FRAMEWORKS := IOKit OpenGL QuartzCore Cocoa 4 | EXECUTABLE := mps 5 | OBJDIR := ./obj 6 | 7 | ARCH_FLAGS := -arch x86_64 8 | CFLAGS := -W -Wall -Wno-unused-parameter -x objective-c -std=gnu99 -fobjc-arc -fno-strict-aliasing -O2 9 | 10 | LIB := $(patsubst %,-framework %,$(notdir $(FRAMEWORKS))) 11 | OBJS += $(patsubst %.m,$(OBJDIR)/%.m.o,$(notdir $(SRCFILES))) 12 | 13 | $(OBJDIR)/%.m.o: $(SRCDIR)%.m 14 | clang $(CFLAGS) $(ARCH_FLAGS) -o $@ -c $< 15 | 16 | $(EXECUTABLE): makedirectories $(OBJS) Makefile 17 | clang $(ARCH_FLAGS) -o $@ $(OBJS) $(LIB) 18 | 19 | makedirectories: 20 | mkdir -p $(OBJDIR) 21 | 22 | clean: 23 | rm -f -r $(OBJDIR) 24 | rm -f $(EXECUTABLE) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2024 Lisa Melton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Source/MediaView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaView.m 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import "MediaView.h" 9 | 10 | @implementation MediaView { 11 | NSView *_videoView; 12 | } 13 | 14 | - (id)initWithFrame:(NSRect)frameRect 15 | { 16 | if (!(self = [super initWithFrame:frameRect])) { 17 | return nil; 18 | } 19 | _displaySize = frameRect.size; 20 | 21 | _videoLayer = [VideoLayer new]; 22 | 23 | _videoView = [[NSView alloc] initWithFrame:frameRect]; 24 | [_videoView setWantsLayer:YES]; 25 | [_videoView setLayer:_videoLayer]; 26 | 27 | [self addSubview:_videoView]; 28 | 29 | return self; 30 | } 31 | 32 | - (void)setFrameSize:(NSSize)newSize 33 | { 34 | [super setFrameSize:newSize]; 35 | 36 | NSRect frameRect; 37 | 38 | CGFloat widthScale = newSize.width / self.displaySize.width; 39 | CGFloat heightScale = newSize.height / self.displaySize.height; 40 | 41 | if (widthScale <= heightScale) { 42 | CGFloat height = self.displaySize.height * widthScale; 43 | frameRect = NSMakeRect( 44 | 0, 45 | (newSize.height - height) / 2, 46 | self.displaySize.width * widthScale, 47 | height 48 | ); 49 | } else { 50 | CGFloat width = self.displaySize.width * heightScale; 51 | frameRect = NSMakeRect( 52 | (newSize.width - width) / 2, 53 | 0, 54 | width, 55 | self.displaySize.height * heightScale 56 | ); 57 | } 58 | [_videoView setFrame:frameRect]; 59 | } 60 | 61 | - (void)drawRect:(NSRect)dirtyRect 62 | { 63 | [[NSColor blackColor] set]; 64 | [NSBezierPath fillRect:dirtyRect]; 65 | } 66 | 67 | - (BOOL)isOpaque 68 | { 69 | return YES; 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Source/VideoLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // VideoLayer.m 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | #import "VideoLayer.h" 10 | 11 | @implementation VideoLayer { 12 | CVOpenGLTextureCacheRef _textureCache; 13 | } 14 | 15 | -(CGLContextObj)copyCGLContextForPixelFormat:(CGLPixelFormatObj)pixelFormat 16 | { 17 | CGLContextObj glContext = [super copyCGLContextForPixelFormat:pixelFormat]; 18 | 19 | CGLLockContext(glContext); 20 | 21 | if (_textureCache) { 22 | CVOpenGLTextureCacheRelease(_textureCache); 23 | } 24 | if (CVOpenGLTextureCacheCreate(NULL, NULL, glContext, pixelFormat, NULL, &_textureCache)) { 25 | _textureCache = NULL; 26 | } 27 | CGLUnlockContext(glContext); 28 | 29 | return glContext; 30 | } 31 | 32 | - (void)drawInCGLContext:(CGLContextObj)glContext 33 | pixelFormat:(CGLPixelFormatObj)pixelFormat 34 | forLayerTime:(CFTimeInterval)timeInterval 35 | displayTime:(const CVTimeStamp *)timeStamp 36 | { 37 | CGLLockContext(glContext); 38 | CGLSetCurrentContext(glContext); 39 | 40 | CVOpenGLTextureRef texture; 41 | 42 | if (self.pixelBuffer && 43 | !CVOpenGLTextureCacheCreateTextureFromImage(NULL, 44 | _textureCache, 45 | self.pixelBuffer, 46 | NULL, 47 | &texture) 48 | ) { 49 | GLenum target = CVOpenGLTextureGetTarget(texture); 50 | 51 | glEnable(target); 52 | glBindTexture(target, CVOpenGLTextureGetName(texture)); 53 | glBegin(GL_QUADS); 54 | 55 | GLfloat width = CVPixelBufferGetWidth(self.pixelBuffer); 56 | GLfloat height = CVPixelBufferGetHeight(self.pixelBuffer); 57 | 58 | glTexCoord2f( 0, 0); glVertex2f(-1, 1); 59 | glTexCoord2f( 0, height); glVertex2f(-1, -1); 60 | glTexCoord2f(width, height); glVertex2f( 1, -1); 61 | glTexCoord2f(width, 0); glVertex2f( 1, 1); 62 | 63 | glEnd(); 64 | glDisable(target); 65 | 66 | CVOpenGLTextureRelease(texture); 67 | } else { 68 | glClearColor(0, 0, 0, 0); 69 | glClear(GL_COLOR_BUFFER_BIT); 70 | } 71 | [super drawInCGLContext:glContext 72 | pixelFormat:pixelFormat 73 | forLayerTime:timeInterval 74 | displayTime:timeStamp]; 75 | 76 | CGLUnlockContext(glContext); 77 | } 78 | 79 | -(void)releaseCGLContext:(CGLContextObj)glContext 80 | { 81 | if (_textureCache) { 82 | CVOpenGLTextureCacheRelease(_textureCache); 83 | _textureCache = NULL; 84 | } 85 | [super releaseCGLContext:glContext]; 86 | } 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /Source/mps.1: -------------------------------------------------------------------------------- 1 | .TH MPS 1 "November 2014" "MPlayerShell 0.9.3" "MPlayerShell Manual" 2 | .SH NAME 3 | .B mps 4 | -- an improved visual experience for MPlayer on OS X 5 | .SH SYNOPSIS 6 | .B mps 7 | [ 8 | .I mplayer arguments 9 | ]... 10 | 11 | MPS_MPLAYER=/path/to/mplayer 12 | .B mps 13 | [ 14 | .I mplayer arguments 15 | ]... 16 | .SH DESCRIPTION 17 | .B MPlayerShell 18 | is an improved visual experience for MPlayer on OS X, launching 19 | .I mplayer 20 | or 21 | .I mplayer2 22 | as a background process, capturing its output, and presenting a whole new application user interface. Its command line interface is essentially identical to MPlayer. 23 | 24 | MPlayerShell also adds explicit menu commands for full-screen and float-on-top modes. Those new menu commands, as well as those for window sizing, are more consistent in appearance and behavior with the standard QuickTime Player application than with MPlayer. 25 | 26 | However, full-screen mode in MPlayerShell doesn't use the animated transition behavior introduced in OS X Lion. It's "instant on" and similar to the mode built into MPlayer itself. 27 | 28 | When MPlayerShell launches MPlayer, it's configured to use a larger cache and leverage multiple processor cores for more threads. This significantly improves performance for Blu-ray Disc-sized video. But even this extra configuration can always be overridden at the command line. 29 | 30 | As long as MPlayerShell is the frontmost application, all the standard MPlayer keyboard shortcuts work even if only audio is playing. 31 | 32 | OS X Lion (version 10.7) or later is required to run MPlayerShell. 33 | .SH OPTIONS 34 | .B MPlayerShell 35 | takes almost all the same options as MPlayer, but there are a few important exceptions: 36 | .TP 37 | .B - 38 | Not allowed because MPlayerShell launches MPlayer in "slave" mode and uses STDIN to send commands to MPlayer. 39 | .TP 40 | .B -vo 41 | Not allowed because MPlayerShell must use a specific video output driver with certain parameters to capture video from MPlayer. 42 | .TP 43 | .B -idle 44 | Ignored because MPlayerShell is the process controlling MPlayer. 45 | .TP 46 | .B -rootwin 47 | Not implemented because it's not particularly useful in MPlayerShell. 48 | .SH EXIT STATUS 49 | .B MPlayerShell 50 | returns as status whatever MPlayer itself returns when it exits in the background. 51 | .SH ENVIRONMENT 52 | .B MPlayerShell 53 | first examines the 54 | .B MPS_MPLAYER 55 | environment variable for the location of 56 | .IR mplayer . 57 | This allows using 58 | .I mplayer2 59 | or other 60 | .I mplayer 61 | executables elsewhere in the file system. If the 62 | .B MPS_MPLAYER 63 | environment variable is undefined or empty, MPlayerShell searches the directories in the 64 | .B PATH 65 | environment variable for 66 | .IR mplayer . 67 | .SH SEE ALSO 68 | .IR mplayer (1), 69 | .IR mplayer2 (1) 70 | .SH COPYRIGHT 71 | Copyright (c) 2013-2024 Lisa Melton 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MPlayerShell 2 | 3 | MPlayerShell is an improved visual experience for MPlayer on OS X. 4 | 5 | ## About 6 | 7 | Hi, I'm [Lisa Melton](http://lisamelton.net/). I wrote MPlayerShell because I was unhappy with the visual experience built into MPlayer on OS X, specifically playing video via the [`mplayer`](http://mplayerhq.hu/) and `mplayer2` command line utilities. 8 | 9 | I love the flexibility and power of MPlayer, but here are some of the problems I was having: 10 | 11 | * Video playback halted during mouse down through the menubar 12 | * White "turds" at the bottom corners of the window and full-screen views (`mplayer` only) 13 | * Incorrect handling of application activation policy on exit (e.g. menubar not being updated) 14 | * Clumsy window sizing not constrained to the video display aspect ratio (`mplayer` only) 15 | * Clumsy window zooming not centered horizontally when invoked the first time 16 | * Inconsistent menu commands (e.g. "Double Size" which only fits the window to the screen) 17 | * Menubar inaccessible within full-screen mode (`mplayer2` only) 18 | 19 | MPlayerShell fixes all of these and other problems by launching MPlayer as a background process, capturing its output, and presenting a whole new application user interface. Its command line interface is essentially identical to MPlayer. 20 | 21 | MPlayerShell also adds explicit menu commands for full-screen and float-on-top modes. Those new menu commands, as well as those for window sizing, are more consistent in appearance and behavior with the standard QuickTime Player application than with MPlayer. 22 | 23 | However, full-screen mode in MPlayerShell doesn't use the animated transition behavior introduced in OS X Lion. It's "instant on" and similar to the mode built into MPlayer itself. 24 | 25 | When MPlayerShell launches MPlayer, it's configured to use a larger cache and leverage multiple processor cores for more threads. This significantly improves performance for Blu-ray Disc-sized video. But even this extra configuration can always be overridden at the command line. 26 | 27 | MPlayerShell is not particularly innovative. It's a small, derivative work meant to scratch my own OCD-driven itch. My hope in publishing MPlayerShell is that 1) it's useful to someone else and 2) both MPlayer development teams incorporate what I've done here into their projects and make mine completely obsolete and unnecessary. 28 | 29 | I also wrote MPlayerShell to relearn Objective-C and Cocoa programming. It's been awhile and I was a little rusty. Since my current plan is to create a real media player application, writing MPlayerShell was great practice to get ready for that project. 30 | 31 | ## Installation 32 | 33 | ### Via Homebrew 34 | 35 | MPlayerShell is available via [Homebrew](http://brew.sh/), a package manager for OS X. 36 | 37 | You can install the latest stable version like this: 38 | 39 | brew install mplayershell 40 | 41 | Or, you can install the bleeding-edge version this way: 42 | 43 | brew install --HEAD mplayershell 44 | 45 | Both provide the MPlayerShell executable, `mps`, and its manual page at the command line. 46 | 47 | ### Caveats 48 | 49 | MPlayerShell needs [`mplayer`](http://mplayerhq.hu/) or `mplayer2` to work properly. The Homebrew installation formula searches the directories in your `PATH` environment variable for executables named `mplayer` and `mplayer2`. If it can't find either, it will install the Homebrew `mplayer` package for you. 50 | 51 | So, if you want a custom build of `mplayer` or `mplayer2`, you need to install it before you install MPlayerShell. 52 | 53 | But not all versions of `mplayer` or `mplayer2` are compatible with MPlayerShell. Custom builds embedded within MPlayerX.app and mplayer2.app should be avoided. I recommend installation via Homebrew. 54 | 55 | The stable version of `mplayer` is available like this: 56 | 57 | brew install mplayer 58 | 59 | Or, you can install the bleeding-edge version of `mplayer` this way: 60 | 61 | brew remove ffmpeg 62 | brew remove mplayer 63 | brew install --HEAD ffmpeg 64 | brew install --HEAD mplayer 65 | 66 | For `mplayer2`, installation instructions via Homebrew are available [here](https://github.com/pigoz/homebrew-mplayer2). 67 | 68 | ### From the source 69 | 70 | MPlayerShell can also be built from the Open Source project code here. 71 | 72 | MPlayerShell is written in Objective-C as an [Xcode](http://developer.apple.com/tools/xcode/) project. You can build it from the command line like this: 73 | 74 | git clone https://github.com/lisamelton/MPlayerShell.git 75 | cd MPlayerShell 76 | xcodebuild 77 | 78 | The MPlayerShell executable, `mps`, should then be available at: 79 | 80 | build/Release/mps 81 | 82 | And it's manual page at: 83 | 84 | Source/mps.1 85 | 86 | You can then then copy those files to wherever your want. 87 | 88 | Or, you can install them into `/usr/local/bin` and `/usr/share/man/man1` like this: 89 | 90 | xcodebuild install 91 | 92 | ## Usage 93 | 94 | mps [ mplayer arguments ]... 95 | 96 | MPS_MPLAYER=/path/to/mplayer mps [ mplayer arguments ]... 97 | 98 | MPlayerShell takes almost all the same options as MPlayer, but there are a few important exceptions: 99 | 100 | * Reading from `STDIN` via the `-` option isn't allowed because MPlayerShell launches MPlayer in "slave" mode and uses `STDIN` to send commands to MPlayer. 101 | * Specifying a video output driver via the `-vo` option isn't allowed because MPlayerShell must use a specific driver with certain parameters to capture video from MPlayer. 102 | * The `-idle` option is ignored since MPlayerShell is the process controlling MPlayer. 103 | * The `-rootwin` option isn't implemented since it's not particularly useful in MPlayerShell. 104 | 105 | MPlayerShell first examines the `MPS_MPLAYER` environment variable for the location of `mplayer`. This allows using `mplayer2` or other `mplayer` executables elsewhere in the file system. If the `MPS_MPLAYER` environment variable is undefined or empty, MPlayerShell searches the directories in the `PATH` environment variable for `mplayer`. 106 | 107 | Of course, `MPS_MPLAYER` can be defined in `~/.bash_profile`, `~/.bashrc`, etc. 108 | 109 | As long as MPlayerShell is the frontmost application, all the standard MPlayer keyboard shortcuts work even if only audio is playing. 110 | 111 | ## Requirements 112 | 113 | OS X Lion (version 10.7) or later is required to run MPlayerShell. 114 | 115 | ## Acknowledgements 116 | 117 | Thanks to Matt Gallagher for his "[Minimalist Cocoa programming](http://www.cocoawithlove.com/2010/09/minimalist-cocoa-programming.html)" blog post which got me thinking about doing this in the first place. 118 | 119 | A big "thank you" to the developers of "[MPlayer OSX Extended](http://www.mplayerosx.ch/)" and "[MPlayerX](http://mplayerx.org/)" whose work gave me some key insights on how to handle the various APIs in MPlayer. 120 | 121 | Of course, tremendous thanks to the MPlayer and mplayer2 development teams for creating such flexible and powerful software. 122 | 123 | Thanks to [Valerii Hiora](http://github.com/vhbit) for providing a Homebrew formula. 124 | 125 | Finally, many thanks to former Apple colleague Ricci Adams of [musictheory.net](http://www.musictheory.net/) for taking me to school on modern Objective-C programming. What a stupid I am. 126 | 127 | ## License 128 | 129 | MPlayerShell is copyright [Lisa Melton](http://lisamelton.net/) and available under a [MIT license](https://github.com/lisamelton/MPlayerShell/blob/master/LICENSE). 130 | -------------------------------------------------------------------------------- /Source/VideoRenderer.m: -------------------------------------------------------------------------------- 1 | // 2 | // VideoRenderer.m 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import "VideoRenderer.h" 9 | 10 | @interface VideoRenderer () 11 | 12 | - (void)connect; 13 | - (void)disconnect; 14 | 15 | @end 16 | 17 | #pragma mark - 18 | 19 | @implementation VideoRenderer { 20 | __weak id _delegate; 21 | NSString *_sharedBufferName; 22 | VideoLayer *_videoLayer; 23 | NSThread *_thread; 24 | size_t _bufferSize; 25 | unsigned char *_sharedBuffer; 26 | unsigned char *_privateBuffer; 27 | CVPixelBufferRef _pixelBuffer; 28 | } 29 | 30 | - (id)initWithDelegate:(id )aDelegate 31 | sharedBufferName:(NSString *)aName 32 | videoLayer:(VideoLayer *)aLayer 33 | { 34 | if (!(self = [super init])) { 35 | return nil; 36 | } 37 | _delegate = aDelegate; 38 | _sharedBufferName = aName; 39 | _videoLayer = aLayer; 40 | 41 | _thread = [[NSThread alloc] initWithTarget:self 42 | selector:@selector(connect) 43 | object:nil]; 44 | [_thread start]; 45 | 46 | return self; 47 | } 48 | 49 | - (void)dealloc 50 | { 51 | if ([_thread isExecuting]) { 52 | [self performSelector:@selector(disconnect) 53 | onThread:_thread 54 | withObject:nil 55 | waitUntilDone:YES]; 56 | } 57 | while ([_thread isExecuting]) { 58 | } 59 | } 60 | 61 | - (void)connect 62 | { 63 | @autoreleasepool { 64 | [NSConnection serviceConnectionWithName:_sharedBufferName 65 | rootObject:self]; 66 | 67 | // There's no advantage using special run loop modes here. Everything 68 | // performs well with the default. Don't use `NSRunLoop` either since 69 | // the call to `CFRunLoopStop` in `disconnect` won't force an 70 | // AppKit-based run loop to exit. 71 | CFRunLoopRun(); 72 | } 73 | } 74 | 75 | - (void)disconnect 76 | { 77 | CFRunLoopStop(CFRunLoopGetCurrent()); 78 | [self stop]; 79 | } 80 | 81 | #pragma mark - 82 | #pragma mark MPlayerOSXVOProto 83 | 84 | - (int)startWithWidth:(bycopy int)width 85 | withHeight:(bycopy int)height 86 | withBytes:(bycopy int)bytes 87 | withAspect:(bycopy int)aspect 88 | { 89 | // The `pixelBuffer` property of the video layer object is used as a 90 | // boolean to indicate whether rendering is in process. Due to the 91 | // unpredictable nature of callback sequencing in `mplayer`, the `stop` 92 | // method is called here if that property is "true." 93 | if (_videoLayer.pixelBuffer) { 94 | [self stop]; 95 | } 96 | int sharedBufferFile = shm_open([_sharedBufferName UTF8String], O_RDONLY, S_IRUSR); 97 | 98 | if (sharedBufferFile == -1) { 99 | NSLog(@"Can't open shared buffer file for mplayer"); 100 | exit(EXIT_FAILURE); 101 | } 102 | _bufferSize = width * height * bytes; 103 | 104 | if (_sharedBuffer) { 105 | munmap(_sharedBuffer, _bufferSize); 106 | } 107 | _sharedBuffer = mmap(NULL, _bufferSize, PROT_READ, MAP_SHARED, sharedBufferFile, 0); 108 | close(sharedBufferFile); 109 | 110 | if (_sharedBuffer == MAP_FAILED) { 111 | NSLog(@"Can't allocate shared buffer for mplayer"); 112 | exit(EXIT_FAILURE); 113 | } 114 | if (_privateBuffer) { 115 | free(_privateBuffer); 116 | } 117 | _privateBuffer = malloc(_bufferSize); 118 | 119 | OSType pixelFormat; 120 | 121 | // Because `mplayer` only passes the size in `bytes` of each pixel, the 122 | // format must be guessed from that value. This guess can be wrong since 123 | // more than one format can apply to a single byte size. But most of the 124 | // time the format will be `kYUVSPixelFormat` anyway. 125 | switch (bytes) { 126 | case 3: 127 | pixelFormat = k24RGBPixelFormat; 128 | break; 129 | case 4: 130 | // Could be `k32ARGBPixelFormat` too, but we can only guess one. 131 | pixelFormat = k32BGRAPixelFormat; 132 | break; 133 | default: 134 | pixelFormat = kYUVSPixelFormat; 135 | } 136 | if (CVPixelBufferCreateWithBytes(NULL, 137 | width, 138 | height, 139 | pixelFormat, 140 | _privateBuffer, 141 | width * bytes, 142 | NULL, 143 | NULL, 144 | NULL, 145 | &_pixelBuffer) 146 | ) { 147 | NSLog(@"Can't allocate pixel buffer"); 148 | exit(EXIT_FAILURE); 149 | } 150 | NSSize displaySize; 151 | 152 | // Use the same egregious algorithm as `mplayer` to derive an "aspect" 153 | // integer from the actual width and height, and then compare it to their 154 | // bogus display `aspect` value. If they're the same, then it's extremely 155 | // likely the image doesn't need to be transformed. 156 | if (((width * 100) / height) == aspect) { 157 | displaySize = NSMakeSize( 158 | width, 159 | height 160 | ); 161 | } else { 162 | CGFloat aspectRatio; 163 | 164 | // The two most common `aspect` values are `133` and `177` because 165 | // they're used in DVD video. So special case those and convert them 166 | // to `4:3` and `16:9` in actual floating point. Otherwise, just 167 | // convert `aspect` into floating point and hope the resulting ratio 168 | // is close to being correct. 169 | switch (aspect) { 170 | case 133: 171 | aspectRatio = 4.0 / 3; 172 | break; 173 | case 177: 174 | aspectRatio = 16.0 / 9; 175 | break; 176 | default: 177 | aspectRatio = aspect / 100.0; 178 | } 179 | if (width <= (height * aspectRatio)) { 180 | displaySize = NSMakeSize( 181 | height * aspectRatio, 182 | height 183 | ); 184 | } else { 185 | displaySize = NSMakeSize( 186 | width, 187 | width / aspectRatio 188 | ); 189 | } 190 | } 191 | dispatch_async(dispatch_get_main_queue(), ^{ 192 | [_delegate startRenderingWithDisplaySize:displaySize]; 193 | }); 194 | 195 | return 0; 196 | } 197 | 198 | - (void)stop 199 | { 200 | // `mplayer` can call `stop` before it calls `startWithWidth:`, so care is 201 | // taken here if that happens. Because `mplayer` is so cavalier about 202 | // callback sequencing, no additional checks are needed to allow 203 | // `disconnect` to call `stop` directly. 204 | if (_videoLayer.pixelBuffer) { 205 | dispatch_async(dispatch_get_main_queue(), ^{ 206 | [_delegate stopRendering]; 207 | }); 208 | _videoLayer.pixelBuffer = NULL; 209 | } 210 | if (_pixelBuffer) { 211 | CVPixelBufferRelease(_pixelBuffer); 212 | _pixelBuffer = NULL; 213 | } 214 | // `free` doesn't object to the occasional NULL pointer. 215 | free(_privateBuffer); 216 | _privateBuffer = NULL; 217 | 218 | if (_sharedBuffer) { 219 | munmap(_sharedBuffer, _bufferSize); 220 | _sharedBuffer = NULL; 221 | } 222 | _bufferSize = 0; 223 | } 224 | 225 | - (void)render 226 | { 227 | if (!_pixelBuffer) { 228 | return; 229 | } 230 | memcpy(_privateBuffer, _sharedBuffer, _bufferSize); 231 | 232 | _videoLayer.pixelBuffer = _pixelBuffer; 233 | [_videoLayer display]; 234 | } 235 | 236 | - (void)toggleFullscreen 237 | { 238 | // Nothing to do here since this will never be called. 239 | } 240 | 241 | - (void)ontop 242 | { 243 | // Nothing to do here since this will never be called either. 244 | } 245 | 246 | @end 247 | -------------------------------------------------------------------------------- /Source/AppController.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppController.m 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import "AppController.h" 9 | 10 | static NSString * const kAppName = @"MPlayerShell"; 11 | 12 | @interface AppController () 13 | 14 | - (void)determineLaunchPath; 15 | - (void)determineArguments; 16 | - (void)determineVideoOutputDriver; 17 | - (void)startTask; 18 | - (void)didTerminateTask; 19 | 20 | @end 21 | 22 | #pragma mark - 23 | 24 | @implementation AppController { 25 | NSString *_launchPath; 26 | NSArray *_arguments; 27 | NSString *_videoOutputDriver; 28 | NSString *_sharedBufferName; 29 | NSTask *_mplayerTask; 30 | } 31 | 32 | - (id)init 33 | { 34 | if (!(self = [super init])) { 35 | return nil; 36 | } 37 | [self determineLaunchPath]; 38 | [self determineArguments]; 39 | [self determineVideoOutputDriver]; 40 | 41 | // mplayer` will write video frames into a named shared buffer. Use the 42 | // process ID to make the buffer name unique. 43 | _sharedBufferName = [NSString stringWithFormat: 44 | @"%@_%d", kAppName, [[NSProcessInfo processInfo] processIdentifier] 45 | ]; 46 | 47 | return self; 48 | } 49 | 50 | - (void)determineLaunchPath 51 | { 52 | // The location of `mplayer` is required to launch it. That can't be 53 | // specified via normal command line arguments since options for this 54 | // program should be identical to `mplayer` for proper emulation. 55 | // 56 | // Instead, allow the location to be passed via the `MPS_MPLAYER` 57 | // environment variable. 58 | // 59 | // If no location is specified, look for `mplayer` in the `PATH` 60 | // environment variable by invoking the shell with a `which mplayer` 61 | // command and parsing its output. 62 | _launchPath = [[[NSProcessInfo processInfo] environment] valueForKey:@"MPS_MPLAYER"]; 63 | 64 | if (_launchPath) { 65 | return; 66 | } 67 | NSTask *task = [NSTask new]; 68 | 69 | [task setStandardInput:[NSPipe pipe]]; 70 | [task setStandardOutput:[NSPipe pipe]]; 71 | [task setStandardError:[NSPipe pipe]]; 72 | 73 | [task setLaunchPath:@"/bin/sh"]; 74 | [task setArguments:@[@"-c", @"which mplayer"]]; 75 | 76 | [task launch]; 77 | [task waitUntilExit]; 78 | 79 | if ([task terminationStatus]) { 80 | NSLog(@"Can't locate mplayer"); 81 | exit(EXIT_FAILURE); 82 | } 83 | NSData *data = [[[task standardOutput] fileHandleForReading] availableData]; 84 | NSUInteger length = [data length]; 85 | 86 | if (length <= 1) { 87 | NSLog(@"Empty path to mplayer"); 88 | exit(EXIT_FAILURE); 89 | } 90 | _launchPath = [[NSString alloc] initWithBytes:[data bytes] 91 | length:(length - 1) 92 | encoding:NSUTF8StringEncoding]; 93 | } 94 | 95 | - (void)determineArguments 96 | { 97 | // Some `mplayer` options are not compatible with this program. Ignore 98 | // idle mode, but fail if the user expects to read from stdin or specifies 99 | // a video output driver. 100 | _arguments = [[NSProcessInfo processInfo] arguments]; 101 | _arguments = [_arguments subarrayWithRange:NSMakeRange(1, [_arguments count] - 1)]; 102 | _arguments = [_arguments filteredArrayUsingPredicate: 103 | [NSPredicate predicateWithFormat:@"(SELF != %@) AND (SELF != %@)", @"-idle", @"--idle"] 104 | ]; 105 | 106 | if ([_arguments containsObject:@"-"]) { 107 | NSLog(@"Reading from stdin not allowed"); 108 | exit(EXIT_FAILURE); 109 | } 110 | if ([_arguments containsObject:@"-vo"] || [_arguments containsObject:@"--vo"]) { 111 | NSLog(@"Video output driver option (-vo) not allowed"); 112 | exit(EXIT_FAILURE); 113 | } 114 | } 115 | 116 | - (void)determineVideoOutputDriver 117 | { 118 | // `mplayer` and `mplayer2` use different video output drivers to write to 119 | // the shared buffer. Parse the output of a "help" command here to 120 | // determine that driver. 121 | NSTask *task = [NSTask new]; 122 | 123 | [task setStandardInput:[NSPipe pipe]]; 124 | [task setStandardOutput:[NSPipe pipe]]; 125 | [task setStandardError:[NSPipe pipe]]; 126 | 127 | [task setLaunchPath:_launchPath]; 128 | [task setArguments:@[@"-vo", @"help"]]; 129 | 130 | [task launch]; 131 | [task waitUntilExit]; 132 | 133 | NSData *data = [[[task standardOutput] fileHandleForReading] availableData]; 134 | 135 | if ([[[NSString alloc] initWithBytesNoCopy:(void *)[data bytes] 136 | length:[data length] 137 | encoding:NSUTF8StringEncoding 138 | freeWhenDone:NO] 139 | rangeOfString:@"\tsharedbuffer\t" 140 | options:NSLiteralSearch 141 | ].length) { 142 | _videoOutputDriver = @"sharedbuffer"; 143 | } else { 144 | _videoOutputDriver = @"corevideo:shared_buffer"; 145 | } 146 | } 147 | 148 | - (void)run 149 | { 150 | [[NSApplication sharedApplication] setDelegate:self]; 151 | 152 | // The `PresentationController` class exists mostly to prevent this class 153 | // from being one huge source file. It also puts all user interface and 154 | // interaction in one place. 155 | PresentationController *presentationController = 156 | [[PresentationController alloc] initWithDelegate:self 157 | appName:kAppName 158 | fullScreenMode:[_arguments containsObject:@"-fs"] || 159 | [_arguments containsObject:@"--fs"] 160 | floatOnTopMode:[_arguments containsObject:@"-ontop"] || 161 | [_arguments containsObject:@"--ontop"]]; 162 | 163 | (void)[[VideoRenderer alloc] initWithDelegate:presentationController 164 | sharedBufferName:_sharedBufferName 165 | videoLayer:presentationController.mediaView.videoLayer]; 166 | 167 | [self startTask]; 168 | 169 | [NSApp run]; 170 | 171 | // Program control flow should never return here. If it does, something 172 | // went horribly wrong. 173 | exit(EXIT_FAILURE); 174 | } 175 | 176 | - (void)startTask 177 | { 178 | // Communication with `mplayer` requires a pipe to `stdin`, but `stdout` 179 | // and `stderr` are left connected to the defaults so console output 180 | // appears like `mplayer` was invoked directly. 181 | // 182 | // Because `mplayer` is placed in slave mode and `stdin` is used to send 183 | // commands, it can no longer be controlled via the keyboard when the 184 | // console is the frontmost application. The `PresentationController` 185 | // class mimics all that behavior when this application is frontmost, even 186 | // when there's no visible video playback window. 187 | // 188 | // In addition to enabling slave mode and specifying a video output driver 189 | // capable of writing frames to a named shared buffer, arguments are also 190 | // passed to set a larger cache size and leverage multiple processor cores 191 | // for more threads. This significantly improves `mplayer` performance for 192 | // Blu-ray Disc-sized video. 193 | // 194 | // With the larger cache size, another argument is passed to set a minimun 195 | // fill percentage to improve performance for network streams by ensuring 196 | // playback starts sooner. 197 | // 198 | // Custom arguments are passed first allowing override of the new cache 199 | // size and thread count. It's not possible to override slave mode or the 200 | // video output driver. 201 | _mplayerTask = [NSTask new]; 202 | 203 | [_mplayerTask setStandardInput:[NSPipe pipe]]; 204 | 205 | [_mplayerTask setLaunchPath:_launchPath]; 206 | [_mplayerTask setArguments:[@[ 207 | @"-slave", 208 | @"-vo", [NSString stringWithFormat:@"%@:buffer_name=%@", _videoOutputDriver, _sharedBufferName], 209 | @"-cache", @"8192", 210 | @"-cache-min", @"1", 211 | @"-lavdopts", [NSString stringWithFormat:@"threads=%lu", [[NSProcessInfo processInfo] processorCount]] 212 | ] arrayByAddingObjectsFromArray:_arguments]]; 213 | 214 | [[NSNotificationCenter defaultCenter] addObserver:self 215 | selector:@selector(didTerminateTask) 216 | name:NSTaskDidTerminateNotification 217 | object:_mplayerTask]; 218 | 219 | [_mplayerTask launch]; 220 | } 221 | 222 | - (void)didTerminateTask 223 | { 224 | // Quit this application when `mplayer` completes playback or if it 225 | // terminates in any other way. 226 | [NSApp terminate:nil]; 227 | } 228 | 229 | #pragma mark - 230 | #pragma mark PresentationControllerDelegate 231 | 232 | - (void)sendCommand:(NSString *)command 233 | { 234 | // mplayer` writes video frames into the named shared buffer and calls 235 | // methods elsewhere in the `VideoRender` class via the `NSDistantObject` 236 | // mechanism. But communication with `mplayer` also requires sending it 237 | // textual commands terminated with a carriage return via `stdin`. 238 | [[[_mplayerTask standardInput] fileHandleForWriting] writeData:[[NSString stringWithFormat:@"%@\n", command] 239 | dataUsingEncoding:NSUTF8StringEncoding]]; 240 | } 241 | 242 | #pragma mark - 243 | #pragma mark NSApplicationDelegate 244 | 245 | - (void)applicationDidFinishLaunching:(NSNotification *)notification 246 | { 247 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 248 | 249 | // Because activation policy has just been set to behave like a real 250 | // application, that policy must be reset on exit to prevent, among other 251 | // things, the menubar created here from remaining on screen. 252 | atexit_b(^ { 253 | [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; 254 | }); 255 | 256 | [NSApp activateIgnoringOtherApps:YES]; 257 | } 258 | 259 | - (void)applicationWillTerminate:(NSNotification *)notification 260 | { 261 | // If this application was quit intentionally then send `mplayer` the same 262 | // command and wait for it to complete. There is the risk that `mplayer` 263 | // may hang here, but in practice this never happens. Waiting is required 264 | // for `mplayer` to exit cleanly and write its normal output to the 265 | // console. 266 | if ([_mplayerTask isRunning]) { 267 | [self sendCommand:@"quit"]; 268 | [_mplayerTask waitUntilExit]; 269 | } 270 | // Since this program is actually a command line tool and not a real Cocoa 271 | // application, exit here with the result returned from `mplayer`. This 272 | // means any remaining objects are not "properly" deallocated. But there 273 | // are no undesirable side effects from such a policy, and it's quite 274 | // practical and speedy because this process simply goes away. 275 | exit([_mplayerTask terminationStatus]); 276 | } 277 | 278 | @end 279 | -------------------------------------------------------------------------------- /MPlayerShell.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F65149CB1745F90D004CC2E9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F65149CA1745F90D004CC2E9 /* main.m */; }; 11 | F65149CF1745F90D004CC2E9 /* mps.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = F65149CE1745F90D004CC2E9 /* mps.1 */; }; 12 | F65149E01745FA19004CC2E9 /* AppController.m in Sources */ = {isa = PBXBuildFile; fileRef = F65149D61745FA19004CC2E9 /* AppController.m */; }; 13 | F65149E11745FA19004CC2E9 /* MediaView.m in Sources */ = {isa = PBXBuildFile; fileRef = F65149D81745FA19004CC2E9 /* MediaView.m */; }; 14 | F65149E21745FA19004CC2E9 /* PresentationController.m in Sources */ = {isa = PBXBuildFile; fileRef = F65149DB1745FA19004CC2E9 /* PresentationController.m */; }; 15 | F65149E31745FA19004CC2E9 /* VideoLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = F65149DD1745FA19004CC2E9 /* VideoLayer.m */; }; 16 | F65149E41745FA19004CC2E9 /* VideoRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = F65149DF1745FA19004CC2E9 /* VideoRenderer.m */; }; 17 | F65149E61745FA52004CC2E9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F65149E51745FA52004CC2E9 /* Cocoa.framework */; }; 18 | F65149E81745FA6B004CC2E9 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F65149E71745FA6B004CC2E9 /* QuartzCore.framework */; }; 19 | F65149EA1745FA77004CC2E9 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F65149E91745FA77004CC2E9 /* OpenGL.framework */; }; 20 | F65149EC1745FA82004CC2E9 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F65149EB1745FA82004CC2E9 /* IOKit.framework */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXCopyFilesBuildPhase section */ 24 | F65149C21745F90D004CC2E9 /* CopyFiles */ = { 25 | isa = PBXCopyFilesBuildPhase; 26 | buildActionMask = 2147483647; 27 | dstPath = /usr/share/man/man1/; 28 | dstSubfolderSpec = 0; 29 | files = ( 30 | F65149CF1745F90D004CC2E9 /* mps.1 in CopyFiles */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 1; 33 | }; 34 | /* End PBXCopyFilesBuildPhase section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | F65149C41745F90D004CC2E9 /* mps */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = mps; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | F65149CA1745F90D004CC2E9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 39 | F65149CD1745F90D004CC2E9 /* MPlayerShell-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MPlayerShell-Prefix.pch"; sourceTree = ""; }; 40 | F65149CE1745F90D004CC2E9 /* mps.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = mps.1; sourceTree = ""; }; 41 | F65149D51745FA19004CC2E9 /* AppController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppController.h; sourceTree = ""; }; 42 | F65149D61745FA19004CC2E9 /* AppController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppController.m; sourceTree = ""; }; 43 | F65149D71745FA19004CC2E9 /* MediaView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaView.h; sourceTree = ""; }; 44 | F65149D81745FA19004CC2E9 /* MediaView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaView.m; sourceTree = ""; }; 45 | F65149D91745FA19004CC2E9 /* MPlayerOSXVOProto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPlayerOSXVOProto.h; sourceTree = ""; }; 46 | F65149DA1745FA19004CC2E9 /* PresentationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PresentationController.h; sourceTree = ""; }; 47 | F65149DB1745FA19004CC2E9 /* PresentationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PresentationController.m; sourceTree = ""; }; 48 | F65149DC1745FA19004CC2E9 /* VideoLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoLayer.h; sourceTree = ""; }; 49 | F65149DD1745FA19004CC2E9 /* VideoLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoLayer.m; sourceTree = ""; }; 50 | F65149DE1745FA19004CC2E9 /* VideoRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoRenderer.h; sourceTree = ""; }; 51 | F65149DF1745FA19004CC2E9 /* VideoRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoRenderer.m; sourceTree = ""; }; 52 | F65149E51745FA52004CC2E9 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 53 | F65149E71745FA6B004CC2E9 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 54 | F65149E91745FA77004CC2E9 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; 55 | F65149EB1745FA82004CC2E9 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | F65149C11745F90D004CC2E9 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | F65149EC1745FA82004CC2E9 /* IOKit.framework in Frameworks */, 64 | F65149EA1745FA77004CC2E9 /* OpenGL.framework in Frameworks */, 65 | F65149E81745FA6B004CC2E9 /* QuartzCore.framework in Frameworks */, 66 | F65149E61745FA52004CC2E9 /* Cocoa.framework in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | F65149BB1745F90D004CC2E9 = { 74 | isa = PBXGroup; 75 | children = ( 76 | F65149C91745F90D004CC2E9 /* Source */, 77 | F65149C61745F90D004CC2E9 /* Frameworks */, 78 | F65149C51745F90D004CC2E9 /* Products */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | F65149C51745F90D004CC2E9 /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | F65149C41745F90D004CC2E9 /* mps */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | F65149C61745F90D004CC2E9 /* Frameworks */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | F65149EB1745FA82004CC2E9 /* IOKit.framework */, 94 | F65149E91745FA77004CC2E9 /* OpenGL.framework */, 95 | F65149E71745FA6B004CC2E9 /* QuartzCore.framework */, 96 | F65149E51745FA52004CC2E9 /* Cocoa.framework */, 97 | ); 98 | name = Frameworks; 99 | sourceTree = ""; 100 | }; 101 | F65149C91745F90D004CC2E9 /* Source */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | F65149D51745FA19004CC2E9 /* AppController.h */, 105 | F65149D61745FA19004CC2E9 /* AppController.m */, 106 | F65149D71745FA19004CC2E9 /* MediaView.h */, 107 | F65149D81745FA19004CC2E9 /* MediaView.m */, 108 | F65149D91745FA19004CC2E9 /* MPlayerOSXVOProto.h */, 109 | F65149DA1745FA19004CC2E9 /* PresentationController.h */, 110 | F65149DB1745FA19004CC2E9 /* PresentationController.m */, 111 | F65149DC1745FA19004CC2E9 /* VideoLayer.h */, 112 | F65149DD1745FA19004CC2E9 /* VideoLayer.m */, 113 | F65149DE1745FA19004CC2E9 /* VideoRenderer.h */, 114 | F65149DF1745FA19004CC2E9 /* VideoRenderer.m */, 115 | F65149CA1745F90D004CC2E9 /* main.m */, 116 | F65149CE1745F90D004CC2E9 /* mps.1 */, 117 | F65149CC1745F90D004CC2E9 /* Supporting Files */, 118 | ); 119 | path = Source; 120 | sourceTree = ""; 121 | }; 122 | F65149CC1745F90D004CC2E9 /* Supporting Files */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | F65149CD1745F90D004CC2E9 /* MPlayerShell-Prefix.pch */, 126 | ); 127 | name = "Supporting Files"; 128 | sourceTree = ""; 129 | }; 130 | /* End PBXGroup section */ 131 | 132 | /* Begin PBXNativeTarget section */ 133 | F65149C31745F90D004CC2E9 /* mps */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = F65149D21745F90D004CC2E9 /* Build configuration list for PBXNativeTarget "mps" */; 136 | buildPhases = ( 137 | F65149C01745F90D004CC2E9 /* Sources */, 138 | F65149C11745F90D004CC2E9 /* Frameworks */, 139 | F65149C21745F90D004CC2E9 /* CopyFiles */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | ); 145 | name = mps; 146 | productName = MPlayerShell; 147 | productReference = F65149C41745F90D004CC2E9 /* mps */; 148 | productType = "com.apple.product-type.tool"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | F65149BC1745F90D004CC2E9 /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | LastUpgradeCheck = 0460; 157 | ORGANIZATIONNAME = "Lisa Melton"; 158 | }; 159 | buildConfigurationList = F65149BF1745F90D004CC2E9 /* Build configuration list for PBXProject "MPlayerShell" */; 160 | compatibilityVersion = "Xcode 3.2"; 161 | developmentRegion = English; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | ); 166 | mainGroup = F65149BB1745F90D004CC2E9; 167 | productRefGroup = F65149C51745F90D004CC2E9 /* Products */; 168 | projectDirPath = ""; 169 | projectRoot = ""; 170 | targets = ( 171 | F65149C31745F90D004CC2E9 /* mps */, 172 | ); 173 | }; 174 | /* End PBXProject section */ 175 | 176 | /* Begin PBXSourcesBuildPhase section */ 177 | F65149C01745F90D004CC2E9 /* Sources */ = { 178 | isa = PBXSourcesBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | F65149CB1745F90D004CC2E9 /* main.m in Sources */, 182 | F65149E01745FA19004CC2E9 /* AppController.m in Sources */, 183 | F65149E11745FA19004CC2E9 /* MediaView.m in Sources */, 184 | F65149E21745FA19004CC2E9 /* PresentationController.m in Sources */, 185 | F65149E31745FA19004CC2E9 /* VideoLayer.m in Sources */, 186 | F65149E41745FA19004CC2E9 /* VideoRenderer.m in Sources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXSourcesBuildPhase section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | F65149D01745F90D004CC2E9 /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 199 | CLANG_CXX_LIBRARY = "libc++"; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_WARN_CONSTANT_CONVERSION = YES; 202 | CLANG_WARN_EMPTY_BODY = YES; 203 | CLANG_WARN_ENUM_CONVERSION = YES; 204 | CLANG_WARN_INT_CONVERSION = YES; 205 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 206 | COPY_PHASE_STRIP = NO; 207 | GCC_C_LANGUAGE_STANDARD = gnu99; 208 | GCC_DYNAMIC_NO_PIC = NO; 209 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 210 | GCC_OPTIMIZATION_LEVEL = 0; 211 | GCC_PREPROCESSOR_DEFINITIONS = ( 212 | "DEBUG=1", 213 | "$(inherited)", 214 | ); 215 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 217 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 219 | GCC_WARN_UNUSED_VARIABLE = YES; 220 | MACOSX_DEPLOYMENT_TARGET = 10.7; 221 | ONLY_ACTIVE_ARCH = YES; 222 | SDKROOT = macosx; 223 | }; 224 | name = Debug; 225 | }; 226 | F65149D11745F90D004CC2E9 /* Release */ = { 227 | isa = XCBuildConfiguration; 228 | buildSettings = { 229 | ALWAYS_SEARCH_USER_PATHS = NO; 230 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 232 | CLANG_CXX_LIBRARY = "libc++"; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_WARN_CONSTANT_CONVERSION = YES; 235 | CLANG_WARN_EMPTY_BODY = YES; 236 | CLANG_WARN_ENUM_CONVERSION = YES; 237 | CLANG_WARN_INT_CONVERSION = YES; 238 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 239 | COPY_PHASE_STRIP = YES; 240 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 241 | GCC_C_LANGUAGE_STANDARD = gnu99; 242 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 243 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 244 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | MACOSX_DEPLOYMENT_TARGET = 10.7; 248 | SDKROOT = macosx; 249 | }; 250 | name = Release; 251 | }; 252 | F65149D31745F90D004CC2E9 /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 256 | GCC_PREFIX_HEADER = "Source/MPlayerShell-Prefix.pch"; 257 | PRODUCT_NAME = "$(TARGET_NAME)"; 258 | }; 259 | name = Debug; 260 | }; 261 | F65149D41745F90D004CC2E9 /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 265 | GCC_PREFIX_HEADER = "Source/MPlayerShell-Prefix.pch"; 266 | PRODUCT_NAME = "$(TARGET_NAME)"; 267 | }; 268 | name = Release; 269 | }; 270 | /* End XCBuildConfiguration section */ 271 | 272 | /* Begin XCConfigurationList section */ 273 | F65149BF1745F90D004CC2E9 /* Build configuration list for PBXProject "MPlayerShell" */ = { 274 | isa = XCConfigurationList; 275 | buildConfigurations = ( 276 | F65149D01745F90D004CC2E9 /* Debug */, 277 | F65149D11745F90D004CC2E9 /* Release */, 278 | ); 279 | defaultConfigurationIsVisible = 0; 280 | defaultConfigurationName = Release; 281 | }; 282 | F65149D21745F90D004CC2E9 /* Build configuration list for PBXNativeTarget "mps" */ = { 283 | isa = XCConfigurationList; 284 | buildConfigurations = ( 285 | F65149D31745F90D004CC2E9 /* Debug */, 286 | F65149D41745F90D004CC2E9 /* Release */, 287 | ); 288 | defaultConfigurationIsVisible = 0; 289 | defaultConfigurationName = Release; 290 | }; 291 | /* End XCConfigurationList section */ 292 | }; 293 | rootObject = F65149BC1745F90D004CC2E9 /* Project object */; 294 | } 295 | -------------------------------------------------------------------------------- /Source/PresentationController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationController.m 3 | // MPlayerShell 4 | // 5 | // Copyright (c) 2013-2024 Lisa Melton 6 | // 7 | 8 | #import 9 | #import 10 | #import "PresentationController.h" 11 | 12 | enum { 13 | kDisplayMinWidth = 160, 14 | kDisplayMinHeight = 90, 15 | }; 16 | 17 | static NSString *fullScreenMenuItemTitle(BOOL inMode) 18 | { 19 | return [NSString stringWithFormat:@"%@ Full Screen", (inMode ? @"Exit" : @"Enter")]; 20 | } 21 | 22 | #pragma mark - 23 | 24 | @interface PresentationController () 25 | 26 | - (void)setupMenus; 27 | - (BOOL)validateMenuItem:(NSMenuItem *)menuItem; 28 | - (void)about; 29 | - (void)toggleFullScreenView:(id)sender; 30 | - (void)toggleFloatOnTopView:(id)sender; 31 | - (void)viewHalfSize; 32 | - (void)viewActualSize; 33 | - (void)viewDoubleSize; 34 | - (void)viewFitToScreen; 35 | - (NSEvent *)handleEvent:(NSEvent *)theEvent; 36 | - (void)hideCursor; 37 | - (void)setFullScreenMode:(BOOL)flag; 38 | - (void)setFloatOnTopMode:(BOOL)flag; 39 | - (void)adjustWindowToSize:(NSSize)aSize 40 | center:(BOOL)centerFlag 41 | animate:(BOOL)animateFlag; 42 | - (NSRect)optimalFrameRectForContentSize:(NSSize)aSize 43 | center:(BOOL)centerFlag; 44 | - (BOOL)isFrameVisbleForContentSize:(NSSize)aSize; 45 | 46 | @end 47 | 48 | #pragma mark - 49 | 50 | @implementation PresentationController { 51 | __weak id _delegate; 52 | NSString *_appName; 53 | BOOL _inFullScreenMode; 54 | BOOL _inFloatOnTopMode; 55 | NSWindow *_mediaWindow; 56 | NSMenuItem *_fullScreenMenuItem; 57 | NSMenuItem *_floatOnTopMenuItem; 58 | NSSize _displaySize; 59 | NSSize _contentSize; 60 | IOPMAssertionID _assertionID; 61 | NSTimer *_cursorTimer; 62 | } 63 | 64 | - (id)initWithDelegate:(id )aDelegate 65 | appName:(NSString *)aString 66 | fullScreenMode:(BOOL)fullScreenFlag 67 | floatOnTopMode:(BOOL)floatOnTopFlag 68 | { 69 | if (!(self = [super init])) { 70 | return nil; 71 | } 72 | _delegate = aDelegate; 73 | _appName = aString; 74 | _inFullScreenMode = fullScreenFlag; 75 | _inFloatOnTopMode = floatOnTopFlag; 76 | 77 | NSSize screenSize = [[NSScreen mainScreen] frame].size; 78 | NSRect contentRect = NSMakeRect( 79 | 0, 80 | 0, 81 | screenSize.width, 82 | screenSize.height 83 | ); 84 | 85 | _mediaView = [[MediaView alloc] initWithFrame:contentRect]; 86 | [_mediaView setTranslatesAutoresizingMaskIntoConstraints:NO]; 87 | 88 | _mediaWindow = [[NSWindow alloc] initWithContentRect:contentRect 89 | styleMask:NSTitledWindowMask | 90 | NSClosableWindowMask | 91 | NSMiniaturizableWindowMask | 92 | NSResizableWindowMask 93 | backing:NSBackingStoreBuffered 94 | defer:NO]; 95 | 96 | [_mediaWindow setDelegate: self]; 97 | [_mediaWindow setContentMinSize:NSMakeSize( 98 | kDisplayMinWidth, 99 | kDisplayMinHeight 100 | )]; 101 | 102 | NSView *contentView = [_mediaWindow contentView]; 103 | [contentView addSubview:_mediaView]; 104 | 105 | NSDictionary *views = NSDictionaryOfVariableBindings(_mediaView); 106 | 107 | [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_mediaView]|" 108 | options:0 109 | metrics:nil 110 | views:views]]; 111 | 112 | [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_mediaView]|" 113 | options:0 114 | metrics:nil 115 | views:views]]; 116 | 117 | [_mediaWindow setTitle: _appName]; 118 | 119 | [self setupMenus]; 120 | 121 | // Install a local monitor because there's not always a visible media 122 | // window to handle events if the user plays only audio. 123 | [NSEvent addLocalMonitorForEventsMatchingMask:(NSMouseMovedMask | NSKeyDownMask) 124 | handler:^(NSEvent *theEvent) { 125 | return [self handleEvent:theEvent]; 126 | }]; 127 | 128 | return self; 129 | } 130 | 131 | - (void)setupMenus 132 | { 133 | // Because nib files are for sissies (and real application bundles). 134 | NSMenu *menubar = [NSMenu new]; 135 | 136 | // App menu 137 | 138 | NSMenuItem *menuItem = [NSMenuItem new]; 139 | [menubar addItem:menuItem]; 140 | 141 | NSMenu *menu = [NSMenu new]; 142 | [menuItem setSubmenu:menu]; 143 | 144 | menuItem = [[NSMenuItem alloc] initWithTitle:[@"About " stringByAppendingString:_appName] 145 | action:@selector(about) 146 | keyEquivalent:@""]; 147 | [menuItem setTarget:self]; 148 | [menu addItem:menuItem]; 149 | 150 | [menu addItem:[NSMenuItem separatorItem]]; 151 | 152 | menuItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:_appName] 153 | action:@selector(hide:) 154 | keyEquivalent:@"h"]; 155 | [menu addItem:menuItem]; 156 | 157 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" 158 | action:@selector(hideOtherApplications:) 159 | keyEquivalent:@"h"]; 160 | [menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask | NSCommandKeyMask)]; 161 | [menu addItem:menuItem]; 162 | 163 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Show All" 164 | action:@selector(unhideAllApplications:) 165 | keyEquivalent:@""]; 166 | [menu addItem:menuItem]; 167 | 168 | [menu addItem:[NSMenuItem separatorItem]]; 169 | 170 | menuItem = [[NSMenuItem alloc] initWithTitle:[@"Quit " stringByAppendingString:_appName] 171 | action:@selector(terminate:) 172 | keyEquivalent:@"q"]; 173 | [menu addItem:menuItem]; 174 | 175 | // View menu 176 | 177 | menuItem = [NSMenuItem new]; 178 | [menubar addItem:menuItem]; 179 | 180 | menu = [[NSMenu alloc] initWithTitle:@"View"]; 181 | [menuItem setSubmenu:menu]; 182 | 183 | // Start with the title blank since `validateMenuItem` will change it soon 184 | // anyway. 185 | menuItem = [[NSMenuItem alloc] initWithTitle:@"" 186 | action:@selector(toggleFullScreenView:) 187 | keyEquivalent:@"f"]; 188 | [menuItem setTarget:self]; 189 | [menuItem setKeyEquivalentModifierMask:(NSControlKeyMask | NSCommandKeyMask)]; 190 | [menu addItem:menuItem]; 191 | _fullScreenMenuItem = menuItem; 192 | 193 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Float on Top" 194 | action:@selector(toggleFloatOnTopView:) 195 | keyEquivalent:@""]; 196 | [menuItem setTarget:self]; 197 | [menu addItem:menuItem]; 198 | _floatOnTopMenuItem = menuItem; 199 | 200 | [menu addItem:[NSMenuItem separatorItem]]; 201 | 202 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Half Size" 203 | action:@selector(viewHalfSize) 204 | keyEquivalent:@"0"]; 205 | [menuItem setTarget:self]; 206 | [menu addItem:menuItem]; 207 | 208 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Actual Size" 209 | action:@selector(viewActualSize) 210 | keyEquivalent:@"1"]; 211 | [menuItem setTarget:self]; 212 | [menu addItem:menuItem]; 213 | 214 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Double Size" 215 | action:@selector(viewDoubleSize) 216 | keyEquivalent:@"2"]; 217 | [menuItem setTarget:self]; 218 | [menu addItem:menuItem]; 219 | 220 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Fit to Screen" 221 | action:@selector(viewFitToScreen) 222 | keyEquivalent:@"3"]; 223 | [menuItem setTarget:self]; 224 | [menu addItem:menuItem]; 225 | 226 | // Window menu 227 | 228 | menuItem = [NSMenuItem new]; 229 | [menubar addItem:menuItem]; 230 | 231 | menu = [[NSMenu alloc] initWithTitle:@"Window"]; 232 | [menuItem setSubmenu:menu]; 233 | 234 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" 235 | action:@selector(performMiniaturize:) 236 | keyEquivalent:@"m"]; 237 | [menu addItem:menuItem]; 238 | 239 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Zoom" 240 | action:@selector(performZoom:) 241 | keyEquivalent:@""]; 242 | [menu addItem:menuItem]; 243 | 244 | [menu addItem:[NSMenuItem separatorItem]]; 245 | 246 | menuItem = [[NSMenuItem alloc] initWithTitle:@"Bring All to Front" 247 | action:@selector(arrangeInFront:) 248 | keyEquivalent:@""]; 249 | [menu addItem:menuItem]; 250 | 251 | // Let NSApp handle the "Windows" menu. 252 | [NSApp setWindowsMenu: menu]; 253 | 254 | [NSApp setMainMenu:menubar]; 255 | } 256 | 257 | - (BOOL)validateMenuItem:(NSMenuItem *)menuItem 258 | { 259 | SEL action = [menuItem action]; 260 | 261 | // "About" is always a valid command. 262 | if (action == @selector(about)) { 263 | return YES; 264 | } 265 | // All other menu items validated here require the media window be visible. 266 | BOOL visible = [_mediaWindow isVisible]; 267 | 268 | if (action == @selector(toggleFullScreenView:)) { 269 | [menuItem setTitle:fullScreenMenuItemTitle(_inFullScreenMode && visible)]; 270 | 271 | } else if (action == @selector(toggleFloatOnTopView:)) { 272 | [menuItem setState:((_inFloatOnTopMode && visible) ? NSOnState : NSOffState)]; 273 | 274 | // Only validate viewing in half, actual and double sizes if the media 275 | // window at those sizes would still fit on the screen. 276 | } else if (action == @selector(viewHalfSize)) { 277 | return visible && !_inFullScreenMode && [self isFrameVisbleForContentSize:NSMakeSize( 278 | _displaySize.width / 2, 279 | _displaySize.height / 2 280 | )]; 281 | 282 | } else if (action == @selector(viewActualSize)) { 283 | return visible && !_inFullScreenMode && [self isFrameVisbleForContentSize:_displaySize]; 284 | 285 | } else if (action == @selector(viewDoubleSize)) { 286 | return visible && !_inFullScreenMode && [self isFrameVisbleForContentSize:NSMakeSize( 287 | _displaySize.width * 2, 288 | _displaySize.height * 2 289 | )]; 290 | 291 | } else if (action == @selector(viewFitToScreen)) { 292 | return visible && !_inFullScreenMode; 293 | } 294 | return visible; 295 | } 296 | 297 | - (void)about 298 | { 299 | // Exit full-screen and float-on-top modes for the "About" command because 300 | // both require the media window to float above all others, obscuring the 301 | // "About" window. 302 | if ([_mediaWindow isVisible]) { 303 | 304 | if (_inFullScreenMode) { 305 | [self toggleFullScreenView:_fullScreenMenuItem]; 306 | } 307 | if (_inFloatOnTopMode) { 308 | [self toggleFloatOnTopView:_floatOnTopMenuItem]; 309 | } 310 | } 311 | [NSApp orderFrontStandardAboutPanelWithOptions:@{ 312 | @"ApplicationName" : _appName, 313 | @"Copyright" : @"Copyright (c) 2013-2024 Lisa Melton", 314 | @"ApplicationVersion" : @"0.9.3", 315 | }]; 316 | } 317 | 318 | - (void)toggleFullScreenView:(id)sender 319 | { 320 | if (![_mediaWindow isVisible]) { 321 | _inFullScreenMode = !_inFullScreenMode; 322 | return; 323 | } 324 | [self setFullScreenMode:!_inFullScreenMode]; 325 | [sender setTitle:fullScreenMenuItemTitle(_inFullScreenMode)]; 326 | } 327 | 328 | - (void)toggleFloatOnTopView:(id)sender 329 | { 330 | if (![_mediaWindow isVisible]) { 331 | _inFloatOnTopMode = !_inFloatOnTopMode; 332 | return; 333 | } 334 | [self setFloatOnTopMode:!_inFloatOnTopMode]; 335 | [sender setState:(_inFloatOnTopMode ? NSOnState : NSOffState)]; 336 | } 337 | 338 | - (void)viewHalfSize 339 | { 340 | [self adjustWindowToSize:NSMakeSize( 341 | _displaySize.width / 2, 342 | _displaySize.height / 2 343 | ) 344 | center:NO 345 | animate:YES]; 346 | } 347 | 348 | - (void)viewActualSize 349 | { 350 | [self adjustWindowToSize:_displaySize 351 | center:NO 352 | animate:YES]; 353 | } 354 | 355 | - (void)viewDoubleSize 356 | { 357 | [self adjustWindowToSize:NSMakeSize( 358 | _displaySize.width * 2, 359 | _displaySize.height * 2 360 | ) 361 | center:NO 362 | animate:YES]; 363 | } 364 | 365 | - (void)viewFitToScreen 366 | { 367 | [self adjustWindowToSize:[[NSScreen mainScreen] frame].size 368 | center:YES 369 | animate:YES]; 370 | } 371 | 372 | - (NSEvent *)handleEvent:(NSEvent *)theEvent 373 | { 374 | if ([theEvent type] == NSMouseMoved) { 375 | 376 | // Show the cursor if in full-screen mode and then set a timer to hide 377 | // it again in 5 seconds. 378 | if (_inFullScreenMode) { 379 | CGDisplayShowCursor(kCGDirectMainDisplay); 380 | [_cursorTimer invalidate]; 381 | _cursorTimer = [NSTimer scheduledTimerWithTimeInterval:5 382 | target:self 383 | selector:@selector(hideCursor) 384 | userInfo:nil 385 | repeats:NO]; 386 | } 387 | // Always pass on mouse moved events. 388 | return theEvent; 389 | } 390 | NSString *characters = [theEvent charactersIgnoringModifiers]; 391 | NSWindow *keyWindow = [NSApp keyWindow]; 392 | 393 | if ([theEvent modifierFlags] & NSCommandKeyMask) { 394 | 395 | // Handle command-w key down equivalent here rather than creating a 396 | // "File" menu with a single "Close" item. 397 | if ([characters isEqualToString:@"w"]) { 398 | [keyWindow performClose:nil]; 399 | return nil; 400 | } 401 | // Ignore remaining command key down events. 402 | return theEvent; 403 | } 404 | // Ignore remaining key down events if a non-media window is key, and 405 | // full-screen or float-on-top modes are not active. 406 | if (keyWindow && (keyWindow != _mediaWindow) && !(_inFullScreenMode || _inFloatOnTopMode)) { 407 | return theEvent; 408 | } 409 | unsigned short keyCode = [theEvent keyCode]; 410 | 411 | // Handle "quit" and "stop" commands here to ensure quitting the 412 | // application uses a control flow consistent with selecting the 413 | // equivalent menu command. 414 | if ([characters isEqualToString:@"q"] || (keyCode == kVK_Escape) || [characters isEqualToString:@"U"]) { 415 | [NSApp terminate:nil]; 416 | return nil; 417 | } 418 | // Handle "vo_fullscreen" and "vo_ontop" commands here to bypass sending 419 | // them to `mplayer` which would just send them back via the 420 | // `toggleFullscreen` or `ontop` selectors in the `VideoRenderer` class. 421 | if ([characters isEqualToString:@"f"]) { 422 | [self toggleFullScreenView:_fullScreenMenuItem]; 423 | return nil; 424 | } 425 | if ([characters isEqualToString:@"T"]) { 426 | [self toggleFloatOnTopView:_floatOnTopMenuItem]; 427 | return nil; 428 | } 429 | // Handle "@" input here to avoid a key value coding-compliant error when 430 | // the `characters` string is passed to `valueForKey:` later. 431 | if ([characters isEqualToString:@"@"]) { 432 | [_delegate sendCommand:@"seek_chapter 1"]; 433 | return nil; 434 | } 435 | NSString *command; 436 | 437 | // Handle the remaining keyboard commands for `mplayer`. Even though a 438 | // particular command might not function in one `mplayer` implementation, 439 | // it could in another. 440 | switch (keyCode) { 441 | case kVK_ANSI_Keypad8: command = @"dvdnav up"; break; 442 | case kVK_ANSI_Keypad2: command = @"dvdnav down"; break; 443 | case kVK_ANSI_Keypad4: command = @"dvdnav left"; break; 444 | case kVK_ANSI_Keypad6: command = @"dvdnav right"; break; 445 | case kVK_ANSI_Keypad5: command = @"dvdnav menu"; break; 446 | case kVK_ANSI_KeypadEnter: command = @"dvdnav select"; break; 447 | case kVK_ANSI_Keypad7: command = @"dvdnav prev"; break; 448 | case kVK_RightArrow: command = @"seek 10"; break; // 10 seconds forward 449 | case kVK_LeftArrow: command = @"seek -10"; break; // 10 seconds backward 450 | case kVK_UpArrow: command = @"seek 60"; break; // 1 minute forward 451 | case kVK_DownArrow: command = @"seek -60"; break; // 1 minute backward 452 | case kVK_PageUp: command = @"seek 600"; break; // 10 minutes forward 453 | case kVK_PageDown: command = @"seek -600"; break; // 10 minutes backward 454 | case kVK_Delete: command = @"speed_set 1.0"; break; 455 | // case kVK_Escape: command = @"quit"; break; 456 | case kVK_Home: command = @"pt_up_step 1"; break; 457 | case kVK_End: command = @"pt_up_step -1"; break; 458 | case kVK_Return: command = @"pt_step 1 1"; break; 459 | case kVK_F13: command = @"alt_src_step 1"; break; // Best equivalent for insert key? 460 | case kVK_ForwardDelete: command = @"alt_src_step -1"; break; 461 | case kVK_Tab: command = @"step_property switch_program"; break; 462 | default: 463 | command = [@{ 464 | @"+" : @"audio_delay 0.100", 465 | @"-" : @"audio_delay -0.100", 466 | @"[" : @"speed_mult 0.9091", 467 | @"]" : @"speed_mult 1.1", 468 | @"{" : @"speed_mult 0.5", 469 | @"}" : @"speed_mult 2.0", 470 | // @"q" : @"quit", 471 | @"p" : @"pause", 472 | @" " : @"pause", 473 | @"." : @"frame_step", 474 | @">" : @"pt_step 1", 475 | @"<" : @"pt_step -1", 476 | @"o" : @"osd", 477 | @"I" : @"osd_show_property_text \"${filename}\"", 478 | @"P" : @"osd_show_progression", 479 | @"z" : @"sub_delay -0.1", 480 | @"x" : @"sub_delay +0.1", 481 | @"g" : @"sub_step -1", 482 | @"y" : @"sub_step +1", 483 | @"9" : @"volume -1", 484 | @"/" : @"volume -1", 485 | @"0" : @"volume 1", 486 | @"*" : @"volume 1", 487 | @"(" : @"balance -0.1", 488 | @")" : @"balance 0.1", 489 | @"m" : @"mute", 490 | @"1" : @"contrast -1", 491 | @"2" : @"contrast 1", 492 | @"3" : @"brightness -1", 493 | @"4" : @"brightness 1", 494 | @"5" : @"hue -1", 495 | @"6" : @"hue 1", 496 | @"7" : @"saturation -1", 497 | @"8" : @"saturation 1", 498 | @"d" : @"frame_drop", 499 | @"D" : @"step_property deinterlace", 500 | @"r" : @"sub_pos -1", 501 | @"t" : @"sub_pos +1", 502 | @"a" : @"sub_alignment", 503 | @"v" : @"sub_visibility", 504 | @"j" : @"sub_select", 505 | @"J" : @"sub_select -3", 506 | @"F" : @"forced_subs_only", 507 | @"#" : @"switch_audio", 508 | @"_" : @"step_property switch_video", 509 | @"i" : @"edl_mark", 510 | @"h" : @"tv_step_channel 1", 511 | @"k" : @"tv_step_channel -1", 512 | @"n" : @"tv_step_norm", 513 | @"u" : @"tv_step_chanlist", 514 | @"X" : @"step_property teletext_mode 1", 515 | @"W" : @"step_property teletext_page 1", 516 | @"Q" : @"step_property teletext_page -1", 517 | // @"T" : @"vo_ontop", 518 | // @"f" : @"vo_fullscreen", 519 | @"c" : @"capturing", 520 | @"s" : @"screenshot 0", 521 | @"S" : @"screenshot 1", 522 | @"w" : @"panscan -0.1", 523 | @"e" : @"panscan +0.1", 524 | @"!" : @"seek_chapter -1", 525 | // @"@" : @"seek_chapter 1", 526 | @"A" : @"switch_angle 1", 527 | // @"U" : @"stop", 528 | } valueForKey:characters]; 529 | } 530 | if (command) { 531 | [_delegate sendCommand:command]; 532 | return nil; 533 | } 534 | return theEvent; 535 | } 536 | 537 | - (void)hideCursor 538 | { 539 | if (_inFullScreenMode) { 540 | CGDisplayHideCursor(kCGDirectMainDisplay); 541 | } 542 | _cursorTimer = nil; 543 | } 544 | 545 | - (void)setFullScreenMode:(BOOL)flag 546 | { 547 | BOOL wasInFullScreenMode = self.mediaView.isInFullScreenMode; 548 | 549 | if (flag) { 550 | 551 | if (!wasInFullScreenMode) { 552 | _contentSize = [self.mediaView frame].size; 553 | 554 | [_mediaWindow setLevel:kCGNormalWindowLevel]; 555 | 556 | [self.mediaView enterFullScreenMode:[NSScreen mainScreen] 557 | withOptions:[NSDictionary dictionaryWithObjectsAndKeys: 558 | [NSNumber numberWithInt:NSFloatingWindowLevel], 559 | NSFullScreenModeWindowLevel, 560 | [NSNumber numberWithUnsignedInt: 561 | (NSApplicationPresentationHideDock | 562 | NSApplicationPresentationAutoHideMenuBar)], 563 | NSFullScreenModeApplicationPresentationOptions, 564 | nil] 565 | ]; 566 | 567 | CGDisplayHideCursor(kCGDirectMainDisplay); 568 | } 569 | } else { 570 | 571 | if (wasInFullScreenMode) { 572 | [self.mediaView exitFullScreenModeWithOptions:nil]; 573 | 574 | // The window level is reset to a normal level when exiting 575 | // full-screen mode. Restore it to floating level if still in 576 | // float-on-top mode. Both modes use the same window level and are 577 | // not mutually exclusive. 578 | if (_inFloatOnTopMode) { 579 | [_mediaWindow setLevel:kCGFloatingWindowLevel]; 580 | } 581 | CGDisplayShowCursor(kCGDirectMainDisplay); 582 | } 583 | } 584 | // Set the full-screen mode flag based on the view state and not the flag 585 | // that was passed into this method. 586 | _inFullScreenMode = self.mediaView.isInFullScreenMode; 587 | 588 | // If previously in full-screen mode, it's possible the media window size 589 | // changed when opening a different video in the playlist. Call 590 | // `adjustWindowToSize:` again using the value of `_contentSize`, which 591 | // was calculated the last time that method was invoked. 592 | if (wasInFullScreenMode && !_inFullScreenMode) { 593 | [self adjustWindowToSize:_contentSize 594 | center:NO 595 | animate:NO]; 596 | } 597 | } 598 | 599 | - (void)setFloatOnTopMode:(BOOL)flag 600 | { 601 | if (flag) { 602 | 603 | if (!_inFullScreenMode) { 604 | [_mediaWindow setLevel:kCGFloatingWindowLevel]; 605 | } 606 | } else { 607 | [_mediaWindow setLevel:kCGNormalWindowLevel]; 608 | } 609 | _inFloatOnTopMode = flag; 610 | } 611 | 612 | - (void)adjustWindowToSize:(NSSize)aSize 613 | center:(BOOL)centerFlag 614 | animate:(BOOL)animateFlag 615 | { 616 | // Start with the optimal size for the media window on screen. 617 | NSRect frameRect = [self optimalFrameRectForContentSize:aSize 618 | center:centerFlag]; 619 | BOOL visible = [_mediaWindow isVisible]; 620 | 621 | if (visible) { 622 | // Save the content size now in case of early return when in 623 | // full-screen mode. 624 | _contentSize = [_mediaWindow contentRectForFrameRect:frameRect].size; 625 | 626 | if (_inFullScreenMode) { 627 | // Force the video view to recalculate its size. Otherwise the 628 | // display aspect ratio of the video may be wrong when in 629 | // full-screen mode. 630 | [self.mediaView setFrameSize:[self.mediaView frame].size]; 631 | 632 | return; 633 | } 634 | if (!centerFlag) 635 | { 636 | // If the window is not already centered on screen, attempt to 637 | // center it over it's previous position instead. 638 | // 639 | // However, if the window was previously the full height of the 640 | // visible screen, center its content over that screen instead. 641 | // This prevents a newly smaller window from being centered 642 | // slightly lower than the middle of the screen due to the menubar. 643 | NSScreen *screen = [NSScreen mainScreen]; 644 | NSRect screenRect = [screen frame]; 645 | NSRect visibleRect = [screen visibleFrame]; 646 | NSRect oldRect = [_mediaWindow frame]; 647 | 648 | frameRect.origin.x = oldRect.origin.x + ((oldRect.size.width - frameRect.size.width) / 2); 649 | 650 | if ((oldRect.origin.y == visibleRect.origin.y) && 651 | (oldRect.size.height == visibleRect.size.height) && 652 | (frameRect.size.height < visibleRect.size.height)) { 653 | frameRect.origin.y = screenRect.origin.y + ((screenRect.size.height - _contentSize.height) / 2); 654 | } else { 655 | frameRect.origin.y = oldRect.origin.y + ((oldRect.size.height - frameRect.size.height) / 2); 656 | } 657 | // Call `constrainFrameRect:` again in case the window was just 658 | // moved offscreen or tucked under the menubar. 659 | frameRect = [_mediaWindow constrainFrameRect:frameRect toScreen:screen]; 660 | } 661 | } 662 | // Reshape and reposition the window, animating between the old and the 663 | // new as necessary. 664 | [_mediaWindow setFrame:frameRect 665 | display:visible 666 | animate:(visible && animateFlag)]; 667 | 668 | // Save the real content size for use later when switching out of 669 | // full-screen mode. 670 | _contentSize = [self.mediaView frame].size; 671 | } 672 | 673 | - (NSRect)optimalFrameRectForContentSize:(NSSize)aSize 674 | center:(BOOL)centerFlag 675 | { 676 | // Constrain width and height to a minimum size. 677 | NSSize adjustedSize = NSMakeSize( 678 | aSize.width > kDisplayMinWidth ? aSize.width : kDisplayMinWidth, 679 | aSize.height > kDisplayMinHeight ? aSize.height : kDisplayMinHeight 680 | ); 681 | NSScreen *screen = [NSScreen mainScreen]; 682 | NSRect screenRect = [screen frame]; 683 | // Constrain width and height to the screen size, otherwise the call to 684 | // `constrainFrameRect:` below may not behave as desired for display 685 | // aspect ratios shorter than the screen. 686 | adjustedSize = NSMakeSize( 687 | adjustedSize.width < screenRect.size.width ? adjustedSize.width : screenRect.size.width, 688 | adjustedSize.height < screenRect.size.height ? adjustedSize.height : screenRect.size.height 689 | ); 690 | NSRect frameRect = [_mediaWindow frameRectForContentRect:NSMakeRect( 691 | 0, 692 | 0, 693 | adjustedSize.width, 694 | adjustedSize.height 695 | )]; 696 | // Use AppKit to constrain the window frame to the visible area of the 697 | // screen and, if necessary, reshape the window to the correct aspect 698 | // ratio which was previously set in `startRenderingWithDisplaySize:` 699 | // below. 700 | frameRect = [_mediaWindow constrainFrameRect:frameRect 701 | toScreen:screen]; 702 | 703 | if (centerFlag) { 704 | // Don't rely on `constrainFrameRect:` to center the window since its 705 | // positioning behavior is a bit capricious. 706 | NSRect visibleRect = [screen visibleFrame]; 707 | NSRect contentRect = [_mediaWindow contentRectForFrameRect:frameRect]; 708 | 709 | if (frameRect.size.width < visibleRect.size.width) { 710 | frameRect.origin.x = (frameRect.origin.x - contentRect.origin.x) + 711 | screenRect.origin.x + ((screenRect.size.width - contentRect.size.width) / 2); 712 | } 713 | if (frameRect.size.height < visibleRect.size.height) { 714 | frameRect.origin.y = (frameRect.origin.y - contentRect.origin.y) + 715 | screenRect.origin.y + ((screenRect.size.height - contentRect.size.height) / 2); 716 | } 717 | } 718 | return frameRect; 719 | } 720 | 721 | - (BOOL)isFrameVisbleForContentSize:(NSSize)aSize 722 | { 723 | NSSize frameSize = [_mediaWindow frameRectForContentRect:NSMakeRect( 724 | 0, 725 | 0, 726 | aSize.width, 727 | aSize.height 728 | )].size; 729 | NSSize visibleSize = [[NSScreen mainScreen] visibleFrame].size; 730 | 731 | return (frameSize.width <= visibleSize.width) && (frameSize.height <= visibleSize.height); 732 | } 733 | 734 | #pragma mark - 735 | #pragma mark VideoRendererDelegate 736 | 737 | - (void)startRenderingWithDisplaySize:(NSSize)aSize 738 | { 739 | _displaySize = aSize; 740 | 741 | // Inform the media view of the true display size in case the window or 742 | // full-screen view is a different shape. 743 | self.mediaView.displaySize = _displaySize; 744 | 745 | // Constrain window sizing to the display shape. The also makes sure any 746 | // later calls to `constrainFrameRect:` do the right thing. 747 | [_mediaWindow setContentAspectRatio:_displaySize]; 748 | 749 | // Center the window on screen if it's not yet visible, otherwise try to 750 | // center it over its previous position. 751 | [self adjustWindowToSize:_displaySize 752 | center:YES 753 | animate:YES]; 754 | 755 | [self setFullScreenMode:_inFullScreenMode]; 756 | [self setFloatOnTopMode:_inFloatOnTopMode]; 757 | 758 | // Bring the window forward and make it visible (if not so already). 759 | [_mediaWindow makeKeyAndOrderFront:nil]; 760 | 761 | if (!_assertionID && 762 | IOPMAssertionCreateWithName( 763 | kIOPMAssertionTypePreventUserIdleDisplaySleep, 764 | kIOPMAssertionLevelOn, 765 | CFSTR("Playing video"), 766 | &_assertionID) 767 | ) { 768 | _assertionID = 0; 769 | } 770 | } 771 | 772 | - (void)stopRendering 773 | { 774 | if (_assertionID) { 775 | IOPMAssertionRelease(_assertionID); 776 | _assertionID = 0; 777 | } 778 | // Force the video layer to erase the last frame. 779 | [self.mediaView.videoLayer setNeedsDisplay]; 780 | } 781 | 782 | #pragma mark - 783 | #pragma mark NSWindowDelegate 784 | 785 | - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame 786 | { 787 | // The default zoomed window frame is often off-center, so return the same 788 | // window frame here used for the "Fit to Screen" command. 789 | return [self optimalFrameRectForContentSize:[[NSScreen mainScreen] frame].size 790 | center:YES]; 791 | } 792 | 793 | - (void)windowWillClose:(NSNotification *)notification 794 | { 795 | // Closing the media window will quit this application. 796 | [NSApp terminate:nil]; 797 | } 798 | 799 | @end 800 | --------------------------------------------------------------------------------