Felix Paul Kühne pushed to branch master at VideoLAN / VLC
Commits: 206556fe by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Add starter lyrics sidebar view components Signed-off-by: Claudio Cambra <[email protected]> - - - - - e58239b7 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Set up lyrics sidebar view Signed-off-by: Claudio Cambra <[email protected]> - - - - - 160a86bc by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Add extra meta name handling in input item Signed-off-by: Claudio Cambra <[email protected]> - - - - - 35255a51 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Present lyrics sidebar if the current item has sylt data Signed-off-by: Claudio Cambra <[email protected]> - - - - - e6b20b73 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Present lyrics in table view with current lyric highlighted Signed-off-by: Claudio Cambra <[email protected]> - - - - - 802726af by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: More efficiently handle standard playback case with lyric updating Signed-off-by: Claudio Cambra <[email protected]> - - - - - e3f2b783 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Add explicit typing to collection types Signed-off-by: Claudio Cambra <[email protected]> - - - - - 540690c2 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Make clicking lyrics move playback to time of lyric Signed-off-by: Claudio Cambra <[email protected]> - - - - - f730bfc4 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Fix type mismatch errors in lyrics sidebar Signed-off-by: Claudio Cambra <[email protected]> - - - - - 646dbf15 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Present lyrics within audio media decorative view, rather than sidebar Signed-off-by: Claudio Cambra <[email protected]> - - - - - a0633f5a by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Provide info on whether track has lyrics in player controller Signed-off-by: Claudio Cambra <[email protected]> - - - - - b5ff79a3 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Add ability to set lyrics showing in player controller Signed-off-by: Claudio Cambra <[email protected]> - - - - - afcca128 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Add lyrics main menu entry Signed-off-by: Claudio Cambra <[email protected]> - - - - - 7e634f11 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Add lyrics button to main video view Signed-off-by: Claudio Cambra <[email protected]> - - - - - 35fde4a3 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Check for lyrics settings in player controller in audio media decorative view Signed-off-by: Claudio Cambra <[email protected]> - - - - - b0bcbf18 by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Fix lyrics insets in the main video view controller Signed-off-by: Claudio Cambra <[email protected]> - - - - - fd2acc9b by Claudio Cambra at 2026-05-06T08:37:43+02:00 macosx: Fix xib crashing on older macOS Signed-off-by: Claudio Cambra <[email protected]> - - - - - 16 changed files: - modules/gui/macosx/UI/MainMenu.xib - modules/gui/macosx/UI/VLCClassicMainVideoView.xib - modules/gui/macosx/UI/VLCMainVideoView.xib - modules/gui/macosx/UI/VLCMainVideoViewAudioMediaDecorativeView.xib - modules/gui/macosx/library/VLCInputItem.h - modules/gui/macosx/library/VLCInputItem.m - modules/gui/macosx/library/VLCLibraryWindowSidebarRootViewController.m - modules/gui/macosx/menus/VLCMainMenu.h - modules/gui/macosx/menus/VLCMainMenu.m - modules/gui/macosx/playqueue/VLCPlayerController.h - modules/gui/macosx/playqueue/VLCPlayerController.m - modules/gui/macosx/windows/controlsbar/VLCMainVideoViewControlsBar.h - modules/gui/macosx/windows/controlsbar/VLCMainVideoViewControlsBar.m - modules/gui/macosx/windows/video/VLCMainVideoViewAudioMediaDecorativeView.h - modules/gui/macosx/windows/video/VLCMainVideoViewAudioMediaDecorativeView.m - modules/gui/macosx/windows/video/VLCMainVideoViewController.m Changes: ===================================== modules/gui/macosx/UI/MainMenu.xib ===================================== @@ -70,6 +70,7 @@ <outlet property="jumpToTime" destination="5138" id="XK6-rV-lr9"/> <outlet property="libraryPlayQueueMode" destination="Zo7-ru-vku" id="uNJ-jw-lG2"/> <outlet property="license" destination="2834" id="0tU-nu-6dP"/> + <outlet property="lyrics" destination="Lyr-Ics-Mni" id="Lyr-Ics-Out"/> <outlet property="mcopyItem" destination="197" id="aBx-7l-f4e"/> <outlet property="messages" destination="1003" id="q4C-pt-kQo"/> <outlet property="minimize" destination="5606" id="tN3-Ho-T0c"/> @@ -431,6 +432,11 @@ <action selector="goToSpecificTime:" target="-2" id="maw-9c-f5Q"/> </connections> </menuItem> + <menuItem title="Lyrics" id="Lyr-Ics-Mni"> + <connections> + <action selector="toggleLyrics:" target="-2" id="Lyr-Ics-Con"/> + </connections> + </menuItem> <menuItem isSeparatorItem="YES" id="5155"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> ===================================== modules/gui/macosx/UI/VLCClassicMainVideoView.xib ===================================== @@ -38,6 +38,7 @@ <outlet property="fullscreenButton" destination="Dka-oG-Ar0" id="nqz-qB-etE"/> <outlet property="jumpBackwardButton" destination="Y6X-Qj-wAU" id="ZDb-ZR-IDF"/> <outlet property="jumpForwardButton" destination="omu-it-6Lt" id="gtC-uD-Mnu"/> + <outlet property="lyricsButton" destination="Lyr-Ics-Btn" id="Lyr-Ics-Out"/> <outlet property="muteVolumeButton" destination="x8O-Jz-KS8" id="lIH-Am-21m"/> <outlet property="pipButton" destination="ha3-L8-lMU" id="nqJ-hd-8M1"/> <outlet property="playButton" destination="etJ-bh-XaL" id="3LP-if-Pah"/> @@ -299,6 +300,16 @@ <stackView distribution="fillEqually" orientation="horizontal" alignment="centerY" spacing="5" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="UCW-iN-Dk1"> <rect key="frame" x="874" y="5" width="148" height="28"/> <subviews> + <button wantsLayer="YES" horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Lyr-Ics-Btn" customClass="VLCImageButton"> + <rect key="frame" x="0.0" y="0.0" width="48" height="28"/> + <buttonCell key="cell" type="push" bezelStyle="rounded" image="music.note.list" catalog="system" imagePosition="only" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Lyr-Ics-Cel"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES" changeBackground="YES" changeGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <action selector="toggleLyrics:" target="3" id="Lyr-Ics-Con"/> + </connections> + </button> <button wantsLayer="YES" horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="ha3-L8-lMU" customClass="VLCImageButton"> <rect key="frame" x="0.0" y="0.0" width="48" height="28"/> <buttonCell key="cell" type="push" bezelStyle="rounded" image="pip.enter" catalog="system" imagePosition="only" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="a9e-eU-lIl"> @@ -334,11 +345,13 @@ <integer value="1000"/> <integer value="1000"/> <integer value="1000"/> + <integer value="1000"/> </visibilityPriorities> <customSpacing> <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> + <real value="3.4028234663852886e+38"/> </customSpacing> </stackView> </subviews> ===================================== modules/gui/macosx/UI/VLCMainVideoView.xib ===================================== @@ -48,6 +48,7 @@ <outlet property="fullscreenButton" destination="dYZ-ri-Kra" id="Cw2-BS-QG9"/> <outlet property="jumpBackwardButton" destination="AXA-01-AU8" id="gfa-Yt-ezI"/> <outlet property="jumpForwardButton" destination="aAq-uE-mLW" id="M7n-mW-tNe"/> + <outlet property="lyricsButton" destination="Lyr-Ics-Btn" id="Lyr-Ics-Out"/> <outlet property="muteVolumeButton" destination="afi-4d-rQk" id="y6R-6o-2OM"/> <outlet property="pipButton" destination="yEi-SZ-SIS" id="vFi-Ln-9lT"/> <outlet property="playButton" destination="PCC-8a-sVF" id="ddT-ZM-Jhz"/> @@ -197,6 +198,16 @@ <action selector="openSubtitlesMenu:" target="3" id="X6e-aG-mVF"/> </connections> </button> + <button wantsLayer="YES" horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Lyr-Ics-Btn" customClass="VLCImageButton"> + <rect key="frame" x="204" y="0.0" width="45" height="28"/> + <buttonCell key="cell" type="recessed" bezelStyle="recessed" image="music.note.list" catalog="system" imagePosition="only" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Lyr-Ics-Cel"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES" changeBackground="YES" changeGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <action selector="toggleLyrics:" target="3" id="Lyr-Ics-Con"/> + </connections> + </button> <button wantsLayer="YES" horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="4tZ-52-1q9" customClass="VLCImageButton"> <rect key="frame" x="204" y="0.0" width="42" height="28"/> <buttonCell key="cell" type="recessed" bezelStyle="recessed" image="bookmark.fill" catalog="system" imagePosition="only" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bGc-fn-jgQ"> @@ -256,6 +267,7 @@ <integer value="1000"/> <integer value="1000"/> <integer value="1000"/> + <integer value="1000"/> </visibilityPriorities> <customSpacing> <real value="3.4028234663852886e+38"/> @@ -266,6 +278,7 @@ <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> + <real value="3.4028234663852886e+38"/> </customSpacing> </stackView> <stackView distribution="fill" orientation="horizontal" alignment="centerY" spacing="5" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zyp-45-IgR"> ===================================== modules/gui/macosx/UI/VLCMainVideoViewAudioMediaDecorativeView.xib ===================================== @@ -30,6 +30,73 @@ </shadow> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="noart" id="m7g-tH-8dl"/> </imageView> + <scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4vZ-4x-fwW"> + <rect key="frame" x="0.0" y="0.0" width="316" height="336"/> + <clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="H6H-Cp-iPh"> + <rect key="frame" x="0.0" y="0.0" width="316" height="336"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="firstColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" typeSelect="NO" rowSizeStyle="automatic" viewBased="YES" id="5mv-Qc-JwM" customClass="VLCPlayQueueTableView"> + <rect key="frame" x="0.0" y="0.0" width="316" height="336"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <size key="intercellSpacing" width="3" height="2"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> + <tableColumns> + <tableColumn identifier="VLCMainVideoViewLyricsTableViewColumnIdentifier" editable="NO" width="284" minWidth="100" maxWidth="2000" id="9yW-DI-c08"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Lyric"> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="JOU-jI-2dC"> + <font key="font" metaFont="message"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES"/> + <prototypeCellViews> + <tableCellView identifier="LyricCellIdentifier" id="ZTp-fs-5fE"> + <rect key="frame" x="11" y="1" width="293" height="44"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Wdd-df-zUi"> + <rect key="frame" x="0.0" y="0.0" width="293" height="44"/> + <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="center" title="Lyric Line" id="deP-dG-o27"> + <font key="font" metaFont="message"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + </subviews> + <constraints> + <constraint firstItem="Wdd-df-zUi" firstAttribute="leading" secondItem="ZTp-fs-5fE" secondAttribute="leading" id="l-lead"/> + <constraint firstItem="Wdd-df-zUi" firstAttribute="trailing" secondItem="ZTp-fs-5fE" secondAttribute="trailing" id="l-trail"/> + <constraint firstItem="Wdd-df-zUi" firstAttribute="top" secondItem="ZTp-fs-5fE" secondAttribute="top" id="l-top"/> + <constraint firstItem="Wdd-df-zUi" firstAttribute="bottom" secondItem="ZTp-fs-5fE" secondAttribute="bottom" id="l-bot"/> + </constraints> + <connections> + <outlet property="textField" destination="Wdd-df-zUi" id="61g-Ze-nvR"/> + </connections> + </tableCellView> + </prototypeCellViews> + </tableColumn> + </tableColumns> + <connections> + <action selector="tableViewAction:" target="WRu-Ic-lQK" id="conn-action-001"/> + </connections> + </tableView> + </subviews> + <nil key="backgroundColor"/> + </clipView> + <scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="L1S-02-OQF"> + <rect key="frame" x="-100" y="-100" width="0.0" height="16"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="IJr-bf-Dsz"> + <rect key="frame" x="300" y="0.0" width="16" height="336"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + </scrollView> </subviews> <constraints> <constraint firstItem="40P-sL-Mzq" firstAttribute="centerX" secondItem="WRu-Ic-lQK" secondAttribute="centerX" id="4Ib-c0-egD"/> @@ -45,12 +112,18 @@ <constraint firstAttribute="bottom" secondItem="D1e-fS-bwO" secondAttribute="bottom" id="toU-5j-lin"/> <constraint firstAttribute="trailing" secondItem="cDv-dn-65H" secondAttribute="trailing" id="vEl-zP-vj9"/> <constraint firstItem="40P-sL-Mzq" firstAttribute="centerY" secondItem="WRu-Ic-lQK" secondAttribute="centerY" priority="250" id="yFl-gc-ITT"/> + <constraint firstAttribute="trailing" secondItem="4vZ-4x-fwW" secondAttribute="trailing" id="Vo4-sT-MAu"/> + <constraint firstAttribute="bottom" secondItem="4vZ-4x-fwW" secondAttribute="bottom" id="aRK-vR-wkV"/> + <constraint firstItem="4vZ-4x-fwW" firstAttribute="top" secondItem="WRu-Ic-lQK" secondAttribute="top" id="ran-Af-QyV"/> + <constraint firstItem="4vZ-4x-fwW" firstAttribute="leading" secondItem="WRu-Ic-lQK" secondAttribute="leading" id="ran-Af-QyW"/> </constraints> <connections> <outlet property="backgroundCoverArtView" destination="cDv-dn-65H" id="bw2-8i-Xg6"/> <outlet property="backgroundVisualEffectView" destination="D1e-fS-bwO" id="Jkh-ep-Wyn"/> <outlet property="foregroundCoverArtView" destination="40P-sL-Mzq" id="nVl-ga-BoM"/> <outlet property="foregroundViewTopConstraint" destination="TVt-Xt-aoo" id="OFh-5C-XZF"/> + <outlet property="lyricsScrollView" destination="4vZ-4x-fwW" id="lyr-scroll-conn"/> + <outlet property="lyricsTableView" destination="5mv-Qc-JwM" id="lyr-table-conn"/> </connections> <point key="canvasLocation" x="66" y="-232"/> </customView> ===================================== modules/gui/macosx/library/VLCInputItem.h ===================================== @@ -77,6 +77,9 @@ extern NSString * const VLCInputItemCommonDataDifferingFlagString; @property (readonly) BOOL preparsed; @property (readonly) BOOL isStream; @property (readonly, nullable) NSArray<NSString *> *options; +@property (readonly) NSArray<NSString *> *extraMetaNames; + +- (nullable NSString *)extraMetaForKey:(NSString *)key; - (void)parseInputItem; - (void)cancelParsing; ===================================== modules/gui/macosx/library/VLCInputItem.m ===================================== @@ -372,6 +372,32 @@ static const struct input_item_parser_cbs_t parserCallbacks = return @""; } +- (NSArray<NSString *> *)extraMetaNames +{ + char **ppsz_names; + const unsigned i_count = input_item_GetMetaExtraNames(_vlcInputItem, &ppsz_names); + if (i_count == 0) { + return @[]; + } + + NSMutableArray * const names = [NSMutableArray arrayWithCapacity:i_count]; + for (unsigned i = 0; i < i_count; i++) { + [names addObject:toNSStr(ppsz_names[i])]; + free(ppsz_names[i]); + } + free(ppsz_names); + + return [names copy]; +} + +- (nullable NSString *)extraMetaForKey:(NSString *)key +{ + char * const psz_value = input_item_GetMetaExtra(_vlcInputItem, [key UTF8String]); + NSString * const returnValue = toNSStr(psz_value); + free(psz_value); + return returnValue; +} + - (vlc_tick_t)duration { return _vlcInputItem->i_duration; ===================================== modules/gui/macosx/library/VLCLibraryWindowSidebarRootViewController.m ===================================== @@ -32,6 +32,7 @@ #import "library/VLCLibraryUIUnits.h" #import "library/VLCLibraryWindow.h" +#import "library/VLCInputItem.h" #import "library/VLCLibraryWindowChaptersSidebarViewController.h" #import "library/VLCLibraryWindowPlayQueueSidebarViewController.h" #import "library/VLCLibraryWindowSidebarChildViewController.h" @@ -89,6 +90,10 @@ selector:@selector(titleListChanged:) name:VLCPlayerTitleListChanged object:nil]; + [notificationCenter addObserver:self + selector:@selector(mediaItemChanged:) + name:VLCPlayerCurrentMediaItemChanged + object:nil]; } - (void)setupPlayQueueTitle @@ -114,10 +119,11 @@ { self.viewSelector.segmentCount = 1; [self.viewSelector setLabel:self.playQueueSidebarViewController.title - forSegment:self.viewSelector.segmentCount - 1]; + forSegment:0]; VLCPlayQueueController * const playQueueController = VLCMain.sharedInstance.playQueueController; VLCPlayerController * const playerController = playQueueController.playerController; + if (playerController.numberOfTitlesOfCurrentMedia > 0) { self.viewSelector.segmentCount++; [self.viewSelector setLabel:self.titlesSidebarViewController.title @@ -160,37 +166,44 @@ [self updateViewSelectorState]; } +- (void)mediaItemChanged:(NSNotification *)notification +{ + [self updateViewSelectorState]; +} + - (void)updateViewSelectorState { [self setupViewSelectorSegments]; - VLCPlayQueueController * const playQueueController = VLCMain.sharedInstance.playQueueController; - VLCPlayerController * const playerController = playQueueController.playerController; - const BOOL titlesEnabled = playerController.numberOfTitlesOfCurrentMedia > 0; - const BOOL chaptersEnabled = playerController.numberOfChaptersForCurrentTitle > 0; - - self.viewSelector.hidden = !chaptersEnabled && !titlesEnabled; - self.topInternalConstraint.active = !self.viewSelector.hidden; + const BOOL showSelector = self.viewSelector.segmentCount > 1; + self.viewSelector.hidden = !showSelector; + self.topInternalConstraint.active = showSelector; + self.playQueueHeaderLabel.hidden = showSelector; const NSLayoutPriority playQueueCompressionPriority = - self.viewSelector.hidden ? NSLayoutPriorityDefaultHigh : NSLayoutPriorityRequired; - self.playQueueHeaderLabel.hidden = chaptersEnabled; + showSelector ? NSLayoutPriorityRequired : NSLayoutPriorityDefaultHigh; [self.playQueueHeaderLabel setContentCompressionResistancePriority:playQueueCompressionPriority forOrientation:NSLayoutConstraintOrientationVertical]; - self.playQueueHeaderTopConstraint.active = !self.playQueueHeaderLabel.hidden; - - NSLayoutConstraint * const counterLabelConstraintToActivate = self.viewSelector.hidden - ? self.counterLabelInHeaderConstraint - : self.counterLabelInChildViewConstraint; - NSLayoutConstraint * const counterLabelConstraintToDeactivate = self.viewSelector.hidden - ? self.counterLabelInChildViewConstraint - : self.counterLabelInHeaderConstraint; - counterLabelConstraintToActivate.active = YES; - counterLabelConstraintToDeactivate.active = NO; - + self.playQueueHeaderTopConstraint.active = !showSelector; + + if (showSelector) { + self.counterLabelInHeaderConstraint.active = NO; + self.counterLabelInChildViewConstraint.active = YES; + } else { + self.counterLabelInHeaderConstraint.active = YES; + self.counterLabelInChildViewConstraint.active = NO; + } + NSString * const selectedSegmentLabel = [self.viewSelector labelForSegment:self.viewSelector.selectedSegment]; - if ((!chaptersEnabled && [selectedSegmentLabel isEqualToString:self.chaptersSidebarViewController.title]) || - (!titlesEnabled && [selectedSegmentLabel isEqualToString:self.titlesSidebarViewController.title])) { + BOOL selectedSegmentValid = NO; + for (NSInteger i = 0; i < self.viewSelector.segmentCount; i++) { + if ([[self.viewSelector labelForSegment:i] isEqualToString:selectedSegmentLabel]) { + selectedSegmentValid = YES; + break; + } + } + + if (!selectedSegmentValid) { self.viewSelector.selectedSegment = 0; [self viewSelectorAction:self.viewSelector]; } ===================================== modules/gui/macosx/menus/VLCMainMenu.h ===================================== @@ -89,6 +89,7 @@ @property (readwrite, weak) IBOutlet NSMenuItem *random; @property (readwrite, weak) IBOutlet NSMenuItem *repeat; @property (readwrite, weak) IBOutlet NSMenuItem *AtoBloop; +@property (readwrite, weak) IBOutlet NSMenuItem *lyrics; @property (readwrite, weak) IBOutlet NSMenuItem *libraryPlayQueueMode; @property (readwrite, weak) IBOutlet NSMenuItem *sortPlayQueue; @property (readwrite, weak) IBOutlet NSMenuItem *quitAfterPB; ===================================== modules/gui/macosx/menus/VLCMainMenu.m ===================================== @@ -385,6 +385,7 @@ typedef NS_ENUM(NSInteger, VLCObjectType) { [_random setTitle: _NS("Random")]; [_repeat setTitle: _NS("Repeat")]; [_AtoBloop setTitle: _NS("A→B Loop")]; + [_lyrics setTitle: _NS("Lyrics")]; [_libraryPlayQueueMode setTitle: _NS("Library Play Queue Mode")]; [_sortPlayQueue setTitle: _NS("Sort Play Queue")]; [_quitAfterPB setTitle: _NS("Quit after Playback")]; @@ -884,6 +885,11 @@ typedef NS_ENUM(NSInteger, VLCObjectType) { [_playerController setABLoop]; } +- (IBAction)toggleLyrics:(id)sender +{ + _playerController.showLyrics = !_playerController.showLyrics; +} + - (IBAction)toggleLibraryPlayQueueMode:(id)sender { _playQueueController.libraryPlayQueueMode = !_playQueueController.libraryPlayQueueMode; @@ -1944,6 +1950,9 @@ typedef NS_ENUM(NSInteger, VLCObjectType) { mi.state = state ? NSOnState : NSOffState; } else if (mi == self.fwd || mi == self.bwd || mi == self.jumpToTime) { return _playerController.currentMedia != nil && _playerController.seekable; + } else if (mi == self.lyrics) { + mi.state = _playerController.showLyrics ? NSOnState : NSOffState; + return _playerController.lyricsAvailable; } else if (mi == self.mute || mi == self.dockMenumute || mi == self.voutMenumute) { mi.state = _playerController.mute ? NSOnState : NSOffState; [self refreshAudioDeviceList]; ===================================== modules/gui/macosx/playqueue/VLCPlayerController.h ===================================== @@ -242,6 +242,18 @@ extern NSString *VLCPlayerVolumeChanged; */ extern NSString *VLCPlayerMuteChanged; +/** + * Listen to VLCPlayerLyricsAvailableChanged to be notified if the current media has synchronized lyrics available + * @note the affected player object will be the object of the notification + */ +extern NSString * const VLCPlayerLyricsAvailableChanged; + +/** + * Listen to VLCPlayerShowLyricsChanged to be notified if the show lyrics user preference changes + * @note the affected player object will be the object of the notification + */ +extern NSString * const VLCPlayerShowLyricsChanged; + extern const CGFloat VLCVolumeMaximum; extern const CGFloat VLCVolumeDefault; @@ -346,6 +358,16 @@ extern const CGFloat VLCVolumeDefault; @property (readonly) BOOL currentMediaIsAudioOnly; +/** + * user preference to show synchronized lyrics instead of artwork during audio playback + */ +@property (readwrite, nonatomic) BOOL showLyrics; + +/** + * indicates if the currently playing media has synchronized lyrics available + */ +@property (readonly) BOOL lyricsAvailable; + /** * the current player state * @return a value according to the vlc_player_state enum ===================================== modules/gui/macosx/playqueue/VLCPlayerController.m ===================================== @@ -82,6 +82,10 @@ NSString *VLCPlayerWallpaperModeChanged = @"VLCPlayerWallpaperModeChanged"; NSString *VLCPlayerListOfVideoOutputThreadsChanged = @"VLCPlayerListOfVideoOutputThreadsChanged"; NSString *VLCPlayerVolumeChanged = @"VLCPlayerVolumeChanged"; NSString *VLCPlayerMuteChanged = @"VLCPlayerMuteChanged"; +NSString * const VLCPlayerLyricsAvailableChanged = @"VLCPlayerLyricsAvailableChanged"; +NSString * const VLCPlayerShowLyricsChanged = @"VLCPlayerShowLyricsChanged"; + +NSString * const VLCPlayerShowLyricsKey = @"VLCPlayerShowLyricsKey"; const CGFloat VLCVolumeMaximum = 2.; const CGFloat VLCVolumeDefault = 1.; @@ -673,6 +677,8 @@ static int BossCallback(vlc_object_t *p_this, _playbackRate = 1.0; + _showLyrics = [NSUserDefaults.standardUserDefaults boolForKey:VLCPlayerShowLyricsKey]; + libvlc_int_t *libvlc = vlc_object_instance(getIntf()); var_AddCallback(libvlc, "intf-boss", BossCallback, (__bridge void *)self); } @@ -737,6 +743,19 @@ static int BossCallback(vlc_object_t *p_this, [_defaultNotificationCenter removeObserver:self]; } +- (void)setShowLyrics:(BOOL)showLyrics +{ + if (_showLyrics == showLyrics) { + return; + } + + _showLyrics = showLyrics; + [NSUserDefaults.standardUserDefaults setBool:showLyrics forKey:VLCPlayerShowLyricsKey]; + + [_defaultNotificationCenter postNotificationName:VLCPlayerShowLyricsChanged + object:self]; +} + - (VLCInputItem *)currentMedia { if (_currentMedia) @@ -818,6 +837,18 @@ static int BossCallback(vlc_object_t *p_this, - (void)metaDataChangedForInput:(input_item_t *)inputItem { + NSString *syltData = nil; + if (_currentMedia) { + syltData = [_currentMedia extraMetaForKey:@"sylt-data"]; + } + + const BOOL lyricsAvailable = (syltData && syltData.length > 0); + if (_lyricsAvailable != lyricsAvailable) { + _lyricsAvailable = lyricsAvailable; + [_defaultNotificationCenter postNotificationName:VLCPlayerLyricsAvailableChanged + object:self]; + } + [_defaultNotificationCenter postNotificationName:VLCPlayerMetadataChangedForCurrentMedia object:self]; } @@ -834,6 +865,19 @@ static int BossCallback(vlc_object_t *p_this, - (void)currentMediaItemChanged:(input_item_t *)newMediaItem { _currentMedia = [[VLCInputItem alloc] initWithInputItem:newMediaItem]; + + NSString *syltData = nil; + if (_currentMedia) { + syltData = [_currentMedia extraMetaForKey:@"sylt-data"]; + } + + const BOOL lyricsAvailable = (syltData && syltData.length > 0); + if (_lyricsAvailable != lyricsAvailable) { + _lyricsAvailable = lyricsAvailable; + [_defaultNotificationCenter postNotificationName:VLCPlayerLyricsAvailableChanged + object:self]; + } + [_defaultNotificationCenter postNotificationName:VLCPlayerCurrentMediaItemChanged object:self]; } ===================================== modules/gui/macosx/windows/controlsbar/VLCMainVideoViewControlsBar.h ===================================== @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readwrite, weak) IBOutlet NSButton *subtitlesButton; @property (readwrite, weak) IBOutlet NSButton *audioButton; @property (readwrite, weak) IBOutlet NSButton *videoButton; +@property (readwrite, weak) IBOutlet NSButton *lyricsButton; @property (readwrite, weak) IBOutlet NSButton *floatOnTopButton; @property (readwrite, weak) IBOutlet NSButton *playbackRateButton; @@ -39,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN - (IBAction)openAudioMenu:(id)sender; - (IBAction)openVideoMenu:(id)sender; - (IBAction)toggleFloatOnTop:(id)sender; +- (IBAction)toggleLyrics:(id)sender; @end ===================================== modules/gui/macosx/windows/controlsbar/VLCMainVideoViewControlsBar.m ===================================== @@ -68,6 +68,9 @@ self.videoButton.toolTip = _NS("Video"); self.videoButton.accessibilityLabel = self.videoButton.toolTip; + self.lyricsButton.toolTip = _NS("Lyrics"); + self.lyricsButton.accessibilityLabel = self.lyricsButton.toolTip; + self.playbackRateButton.toolTip = _NS("Playback Rate"); self.playbackRateButton.accessibilityLabel = self.playbackRateButton.toolTip; @@ -91,6 +94,7 @@ [buttons addPointer:(__bridge void *)self.subtitlesButton]; [buttons addPointer:(__bridge void *)self.audioButton]; [buttons addPointer:(__bridge void *)self.videoButton]; + [buttons addPointer:(__bridge void *)self.lyricsButton]; [buttons addPointer:(__bridge void *)self.fullscreenButton]; [buttons addPointer:(__bridge void *)self.floatOnTopButton]; [buttons addPointer:(__bridge void *)self.playbackRateButton]; @@ -133,6 +137,14 @@ selector:@selector(updateAvailableButtons:) name:VLCPlayerCurrentMediaItemChanged object:nil]; + [notificationCenter addObserver:self + selector:@selector(updateLyricsButton:) + name:VLCPlayerShowLyricsChanged + object:nil]; + [notificationCenter addObserver:self + selector:@selector(updateLyricsButton:) + name:VLCPlayerLyricsAvailableChanged + object:nil]; [self update]; } @@ -142,6 +154,7 @@ [super update]; [self updateFloatOnTopButton]; [self updatePlaybackRateButton]; + [self updateLyricsButton:nil]; } - (void)floatOnTopChanged:(NSNotification *)notification @@ -149,6 +162,21 @@ [self updateFloatOnTopButton]; } +- (void)updateLyricsButton:(NSNotification *)notification +{ + const BOOL lyricsAvailable = _playerController.lyricsAvailable; + self.lyricsButton.enabled = lyricsAvailable; + self.lyricsButton.state = _playerController.showLyrics ? NSControlStateValueOn : NSControlStateValueOff; + + if (@available(macOS 26.0, *)) { + self.lyricsButton.bezelColor = + _playerController.showLyrics ? NSColor.controlAccentColor : nil; + } else if (@available(macOS 10.14, *)) { + self.lyricsButton.contentTintColor = + _playerController.showLyrics ? NSColor.controlAccentColor : nil; + } +} + - (vout_thread_t *)windowVoutThread { NSWindow * const window = self.floatOnTopButton.window; @@ -277,6 +305,11 @@ vout_Release(p_vout); } +- (IBAction)toggleLyrics:(id)sender +{ + _playerController.showLyrics = !_playerController.showLyrics; +} + - (void)updateAvailableButtons:(id)sender { const BOOL currentItemIsAudio = _playerController.currentMediaIsAudioOnly; ===================================== modules/gui/macosx/windows/video/VLCMainVideoViewAudioMediaDecorativeView.h ===================================== @@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN @property (readwrite, strong) IBOutlet NSVisualEffectView *backgroundVisualEffectView; @property (readwrite, strong) IBOutlet NSLayoutConstraint *foregroundViewTopConstraint; +@property (readwrite, weak) IBOutlet NSScrollView *lyricsScrollView; +@property (readwrite, weak) IBOutlet NSTableView *lyricsTableView; + @end NS_ASSUME_NONNULL_END ===================================== modules/gui/macosx/windows/video/VLCMainVideoViewAudioMediaDecorativeView.m ===================================== @@ -22,16 +22,26 @@ #import "VLCMainVideoViewAudioMediaDecorativeView.h" +#import "extensions/NSColor+VLCAdditions.h" +#import "extensions/NSFont+VLCAdditions.h" #import "extensions/NSView+VLCAdditions.h" #import "main/VLCMain.h" #import "library/VLCLibraryDataTypes.h" #import "library/VLCLibraryImageCache.h" +#import "library/VLCInputItem.h" #import "playqueue/VLCPlayQueueController.h" #import "playqueue/VLCPlayerController.h" +@interface VLCMainVideoViewAudioMediaDecorativeView () <NSTableViewDataSource, NSTableViewDelegate> + +@property (readwrite) NSArray<NSDictionary<NSString *, id> *> *lyricsEntries; +@property (readwrite) NSInteger currentLyricIndex; + +@end + @implementation VLCMainVideoViewAudioMediaDecorativeView + (instancetype)fromNibWithOwner:(id)owner @@ -43,17 +53,51 @@ - (void)awakeFromNib { + _lyricsEntries = @[]; + _currentLyricIndex = -1; + + self.lyricsTableView.dataSource = self; + self.lyricsTableView.delegate = self; + self.lyricsTableView.headerView = nil; + self.lyricsTableView.backgroundColor = [NSColor clearColor]; + self.lyricsTableView.selectionHighlightStyle = NSTableViewSelectionHighlightStyleNone; + NSNotificationCenter * const notificationCenter = NSNotificationCenter.defaultCenter; [notificationCenter addObserver:self - selector:@selector(playerCurrentMediaItemChanged:) + selector:@selector(playerMetadataChanged:) + name:VLCPlayerMetadataChangedForCurrentMedia + object:nil]; + [notificationCenter addObserver:self + selector:@selector(playerMetadataChanged:) name:VLCPlayerCurrentMediaItemChanged object:nil]; + [notificationCenter addObserver:self + selector:@selector(playerTimeChanged:) + name:VLCPlayerTimeAndPositionChanged + object:nil]; + [notificationCenter addObserver:self + selector:@selector(playerLyricsPreferenceChanged:) + name:VLCPlayerShowLyricsChanged + object:nil]; + [self updateCoverArt]; + [self updateLyricsData]; } -- (void)playerCurrentMediaItemChanged:(NSNotification *)notification +- (void)playerMetadataChanged:(NSNotification *)notification { [self updateCoverArt]; + [self updateLyricsData]; +} + +- (void)playerLyricsPreferenceChanged:(NSNotification *)notification +{ + [self updateLyricsData]; +} + +- (void)playerTimeChanged:(NSNotification *)notification +{ + [self updateCurrentLyric]; } - (void)updateCoverArt @@ -79,4 +123,183 @@ _foregroundCoverArtView.image = coverArtImage; } +- (void)updateLyricsData +{ + VLCPlayerController * const playerController = + VLCMain.sharedInstance.playQueueController.playerController; + + NSString *syltData = nil; + if (playerController.showLyrics && playerController.lyricsAvailable) { + syltData = [playerController.currentMedia extraMetaForKey:@"sylt-data"]; + } + + if (!syltData || syltData.length == 0) { + self.lyricsEntries = @[]; + self.currentLyricIndex = -1; + [self.lyricsTableView reloadData]; + self.lyricsScrollView.hidden = YES; + self.foregroundCoverArtView.hidden = NO; + return; + } + + NSMutableArray * const entries = [NSMutableArray array]; + NSArray<NSString *> * const rawEntries = [syltData componentsSeparatedByString:@"\x1E"]; + for (NSString * const rawEntry in rawEntries) { + if (rawEntry.length == 0) { + continue; + } + NSArray<NSString *> * const parts = [rawEntry componentsSeparatedByString:@"\x1F"]; + if (parts.count >= 2) { + NSDictionary<NSString *, id> * const entry = @{ + @"time": @([parts[0] longLongValue]), + @"text": parts[1] + }; + [entries addObject:entry]; + } + } + + self.lyricsEntries = [entries copy]; + self.currentLyricIndex = -1; + [self.lyricsTableView reloadData]; + + const BOOL hasLyrics = self.lyricsEntries.count > 0; + self.lyricsScrollView.hidden = !hasLyrics; + self.foregroundCoverArtView.hidden = hasLyrics; + + [self updateCurrentLyric]; +} + +- (void)updateCurrentLyric +{ + const NSInteger lyricCount = (NSInteger)self.lyricsEntries.count; + if (lyricCount == 0) { + return; + } + + VLCPlayerController * const playerController = + VLCMain.sharedInstance.playQueueController.playerController; + const vlc_tick_t currentTime = playerController.time; + const long long currentTimeMs = MS_FROM_VLC_TICK(currentTime); + + // 1. Check if we are still on the same lyric (most frequent case) + if (self.currentLyricIndex >= 0 && self.currentLyricIndex < lyricCount) { + const long long startTime = + [self.lyricsEntries[self.currentLyricIndex][@"time"] longLongValue]; + const BOOL isLast = (self.currentLyricIndex == lyricCount - 1); + const long long nextStartTime = isLast + ? LLONG_MAX + : [self.lyricsEntries[self.currentLyricIndex + 1][@"time"] longLongValue]; + + if (currentTimeMs >= startTime && currentTimeMs < nextStartTime) { + return; + } + + // 2. Check if it's simply the next one (normal linear playback transition) + if (!isLast && currentTimeMs >= nextStartTime) { + const NSInteger nextIndex = self.currentLyricIndex + 1; + const BOOL isNextLast = (nextIndex == lyricCount - 1); + const long long nextNextStartTime = isNextLast + ? LLONG_MAX + : [self.lyricsEntries[nextIndex + 1][@"time"] longLongValue]; + + if (currentTimeMs < nextNextStartTime) { + [self updateToLyricIndex:nextIndex]; + return; + } + } + } + + // 3. Fallback to search (for seeks, jumps, or initial playback) + NSInteger foundIndex = -1; + for (NSInteger i = 0; i < lyricCount; i++) { + if ([self.lyricsEntries[i][@"time"] longLongValue] <= currentTimeMs) { + foundIndex = i; + } else { + break; + } + } + + if (foundIndex != self.currentLyricIndex) { + [self updateToLyricIndex:foundIndex]; + } +} + +- (void)updateToLyricIndex:(NSInteger)newIndex +{ + const NSInteger lyricCount = (NSInteger)self.lyricsEntries.count; + NSMutableIndexSet * const indicesToUpdate = [NSMutableIndexSet indexSet]; + if (self.currentLyricIndex != -1 && self.currentLyricIndex < lyricCount) { + [indicesToUpdate addIndex:self.currentLyricIndex]; + } + + self.currentLyricIndex = newIndex; + + if (self.currentLyricIndex != -1 && self.currentLyricIndex < lyricCount) { + [indicesToUpdate addIndex:self.currentLyricIndex]; + }; + + if (indicesToUpdate.count > 0) { + [self.lyricsTableView noteHeightOfRowsWithIndexesChanged:indicesToUpdate]; + [self.lyricsTableView reloadDataForRowIndexes:indicesToUpdate + columnIndexes:[NSIndexSet indexSetWithIndex:0]]; + } + + if (self.currentLyricIndex != -1) { + [self.lyricsTableView scrollRowToVisible:self.currentLyricIndex]; + } +} + +#pragma mark - Table View Data Source + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return (NSInteger)self.lyricsEntries.count; +} + +#pragma mark - Table View Delegate + +- (NSView *)tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row +{ + NSTableCellView * const cellView = + [tableView makeViewWithIdentifier:@"LyricCellIdentifier" owner:self]; + if (row < 0 || row >= (NSInteger)self.lyricsEntries.count) { + return cellView; + } + + NSString * const text = self.lyricsEntries[row][@"text"]; + cellView.textField.stringValue = text; + + if (row == self.currentLyricIndex) { + cellView.textField.font = [NSFont boldSystemFontOfSize:22.0]; + cellView.textField.textColor = [NSColor labelColor]; + } else { + cellView.textField.font = [NSFont systemFontOfSize:16.0]; + cellView.textField.textColor = [NSColor secondaryLabelColor]; + } + + return cellView; +} + +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row +{ + return (row == self.currentLyricIndex) ? 60.0 : 40.0; +} + +- (IBAction)tableViewAction:(id)sender +{ + const NSInteger clickedRow = self.lyricsTableView.clickedRow; + if (clickedRow < 0 || clickedRow >= (NSInteger)self.lyricsEntries.count) { + return; + } + + const long long lyricTimeMs = [self.lyricsEntries[clickedRow][@"time"] longLongValue]; + const vlc_tick_t vlcTime = VLC_TICK_FROM_MS(lyricTimeMs); + + VLCPlayerController * const playerController = + VLCMain.sharedInstance.playQueueController.playerController; + [playerController setTimePrecise:vlcTime]; +} + @end ===================================== modules/gui/macosx/windows/video/VLCMainVideoViewController.m ===================================== @@ -172,6 +172,7 @@ NSString * const VLCUseClassicVideoPlayerLayoutKey = @"VLCUseClassicVideoPlayerL // Only set up button constraints for standard layout _bottomButtonStackViewConstraint = [self.bottomBarView.topAnchor constraintEqualToAnchor:self.centralControlsStackView.bottomAnchor]; + [self updateLyricsInsets]; } VLCPlayerController * const controller = @@ -219,6 +220,7 @@ NSString * const VLCUseClassicVideoPlayerLayoutKey = @"VLCUseClassicVideoPlayerL [self setupAudioDecorativeView]; [self.controlsBar update]; [self updateFloatOnTopIndicator]; + [self updateLyricsInsets]; // For classic layout, create constraint for video view to fill main view when controls are hidden if (self.classic) { @@ -245,6 +247,17 @@ NSString * const VLCUseClassicVideoPlayerLayoutKey = @"VLCUseClassicVideoPlayerL } } +- (void)updateLyricsInsets +{ + const CGFloat topInset = self.fakeTitleBar.frame.size.height + VLCLibraryUIUnits.largeSpacing; + const CGFloat bottomInset = self.bottomBarView.frame.size.height + VLCLibraryUIUnits.largeSpacing; + + NSScrollView * const scrollView = self.audioDecorativeView.lyricsScrollView; + scrollView.automaticallyAdjustsContentInsets = NO; + scrollView.contentInsets = NSEdgeInsetsMake(topInset, 0, bottomInset, 0); + scrollView.scrollerInsets = NSEdgeInsetsMake(topInset, 0, 0, 0); +} + - (BOOL)isVisualizationActive { BOOL isVisualActive; View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/d0a89f94fb0003aa41b23b6b259946242f8f0476...fd2acc9bfc113600f6ba6447c02507eb4f193d13 -- View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/d0a89f94fb0003aa41b23b6b259946242f8f0476...fd2acc9bfc113600f6ba6447c02507eb4f193d13 You're receiving this email because of your account on code.videolan.org.
_______________________________________________ vlc-commits mailing list [email protected] https://mailman.videolan.org/listinfo/vlc-commits
