diff --git a/modules/video_output/apple/VLCSampleBufferDisplay.m b/modules/video_output/apple/VLCSampleBufferDisplay.m index f13a4f4f8a..567a03a7b1 100644 --- a/modules/video_output/apple/VLCSampleBufferDisplay.m +++ b/modules/video_output/apple/VLCSampleBufferDisplay.m @@ -140,7 +140,7 @@ typedef NS_ENUM(NSUInteger, VLCSampleBufferPixelFlip) { kCVPixelBufferMetalCompatibilityKey, #if TARGET_OS_OSX kCVPixelBufferOpenGLCompatibilityKey, -#elif !defined(TARGET_OS_VISION) || !TARGET_OS_VISION +#elif (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION) && !TARGET_OS_MACCATALYST kCVPixelBufferOpenGLESCompatibilityKey, #endif }; @@ -151,7 +151,7 @@ typedef NS_ENUM(NSUInteger, VLCSampleBufferPixelFlip) { (__bridge CFNumberRef)(@(dstHeight)), (__bridge CFDictionaryRef)@{}, kCFBooleanTrue, -#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION +#if (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION) && !TARGET_OS_MACCATALYST kCFBooleanTrue #endif }; @@ -660,8 +660,59 @@ shouldInheritContentsScale:(CGFloat)newScale - (instancetype)initWithVoutDisplay:(vout_display_t *)vd; @end +/* The sample-buffer layer fits its enqueued frames by gravity, not by the + * core-computed place rectangle, so the display fitting mode is mapped to a + * gravity: the larger ("fit outside") mode covers and crops, every other mode + * preserves aspect inside the surface. */ +static AVLayerVideoGravity VLCGravityForDisplayCfg(const vout_display_cfg_t *cfg) +{ + if (cfg->display.fitting == VLC_VIDEO_FIT_LARGER) + return AVLayerVideoGravityResizeAspectFill; + return AVLayerVideoGravityResizeAspect; +} + @implementation VLCSampleBufferDisplay +/* A forced display aspect (libvlc_video_set_aspect_ratio) reaches the vout as a + * target place rectangle, but AVSampleBufferDisplayLayer still fits the natural + * pixel shape. Scale the natural aspect-fit box into VLC's computed place box so + * forced ratios visibly stretch while the default path remains identity. The + * fill (cover) path keeps its identity transform and uses layer gravity. */ +- (void)applyAspectStretch +{ + AVSampleBufferDisplayLayer *layer = self.displayLayer; + if (!layer) + return; + const vout_display_place_t *p = self.vd->place; + const video_format_t *source = self.vd->source; + const vout_display_cfg_t *cfg = self.vd->cfg; + CGAffineTransform t = CGAffineTransformIdentity; + if (source && cfg && p->width > 0 && p->height > 0 + && cfg->display.width > 0 && cfg->display.height > 0 + && source->i_visible_width > 0 && source->i_visible_height > 0 + && cfg->display.fitting != VLC_VIDEO_FIT_LARGER) + { + const double displayW = (double)cfg->display.width; + const double displayH = (double)cfg->display.height; + const double naturalAR = + (double)source->i_visible_width / (double)source->i_visible_height; + const double displayAR = displayW / displayH; + double naturalW; + double naturalH; + if (naturalAR > displayAR) { + naturalW = displayW; + naturalH = displayW / naturalAR; + } else { + naturalW = displayH * naturalAR; + naturalH = displayH; + } + if (naturalW > 0.0 && naturalH > 0.0) + t = CGAffineTransformMakeScale((double)p->width / naturalW, + (double)p->height / naturalH); + } + layer.affineTransform = t; +} + - (id)rotationContext { if (_rotationContext) @@ -742,6 +793,8 @@ shouldInheritContentsScale:(CGFloat)newScale @synchronized(sys.displayLayer) { sys.displayLayer = displayView.displayLayer; } + sys.displayLayer.videoGravity = VLCGravityForDisplayCfg(sys.vd->cfg); + [sys applyAspectStretch]; [sys preparePictureInPicture]; }); } @@ -1055,7 +1108,16 @@ static int Control (vout_display_t *vd, int query) case VOUT_DISPLAY_CHANGE_SOURCE_ASPECT: case VOUT_DISPLAY_CHANGE_SOURCE_CROP: case VOUT_DISPLAY_CHANGE_SOURCE_PLACE: + { + AVLayerVideoGravity gravity = VLCGravityForDisplayCfg(vd->cfg); + dispatch_async(dispatch_get_main_queue(), ^{ + @synchronized(sys.displayLayer) { + sys.displayLayer.videoGravity = gravity; + } + [sys applyAspectStretch]; + }); break; + } default: msg_Err (vd, "Unhandled request %d", query); return VLC_EGENERIC;