Skip to content

Commit a4a485b

Browse files
committed
Add opus audio preview support
Add owned .opus metadata to the host app and extension so Finder can route audio-only Opus files through MKVQuickLook. Introduce an audio-only preview mode that keeps playback controls visible without showing an empty video surface, and add tests for the new metadata and UI behavior. Also stop the previous player before replacing it when Finder switches to another file, preventing stale audio from continuing in the background. Update the README with a prominent Gatekeeper note and document the new audio-only support, and record the change in the changelog.
1 parent 1c3e930 commit a4a485b

11 files changed

Lines changed: 208 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Added
6+
7+
- Added owned `.opus` audio support in the app and extension metadata.
8+
- Added an audio-only Quick Look mode that keeps playback controls visible without showing an empty video frame.
9+
10+
### Fixed
11+
12+
- Switching to another file while a preview is already open now stops the previous player before the new one is attached, preventing stray audio from continuing in the background.
13+
14+
### Notes
15+
16+
- `.opus` is audio-only support, not video support hidden behind another extension.
17+
318
## 0.1.3 - 2026-04-09
419

520
### Changed

MKVQuickLook.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
0185A1E4CAD0F2536931685A /* VLCVideoLayerHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21ACF4081841AC895F797878 /* VLCVideoLayerHostView.swift */; };
11+
04AD3364848C6C9F1181F5D9 /* MediaPreviewPlayerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70C1B6AEE09C5CEE98DA7B2 /* MediaPreviewPlayerSession.swift */; };
1112
05ED2B3AB06CF53FBE41C024 /* VLCKitMediaPreviewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C0D0E7E3433A3474B1CA13 /* VLCKitMediaPreviewPlayer.swift */; };
1213
09D842054D1327F0CDE6FC1D /* VLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 26CCAD556DF88A26642D0486 /* VLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1314
1BADCE5D6CF31C5D0C3EA530 /* VLCVideoLayerHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21ACF4081841AC895F797878 /* VLCVideoLayerHostView.swift */; };
@@ -22,6 +23,7 @@
2223
3A5AC45C4441D15E3D0021C5 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9165C63B0A4167A6AFEAA0 /* PreviewViewController.swift */; };
2324
3C2B58691ED0D8F1CEFD730F /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E1EE6E8B183958E809ED768 /* VideoToolbox.framework */; };
2425
3DA12CC066C56B61251996AB /* VideoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82538038139633F4454BF3B8 /* VideoLayout.swift */; };
26+
3E4C4A83A07B01771B7AF288 /* MediaPreviewPlayerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70C1B6AEE09C5CEE98DA7B2 /* MediaPreviewPlayerSession.swift */; };
2527
4F70B1768253CC6D0CC94E8B /* MainWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6820EDD7105300E0B3C6FE /* MainWindowView.swift */; };
2628
509B4B27E2803EB4E4308002 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71B770A4F3058220E670AE4D /* AVFoundation.framework */; };
2729
5309DF9B48353020DDF8B815 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40C73021684F36F46E4F3AE6 /* Quartz.framework */; };
@@ -53,6 +55,7 @@
5355
C97CB0AA2C6918F5835F1B7B /* MediaPreviewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9555F7EA3CD226932F56DAAE /* MediaPreviewPlayer.swift */; };
5456
CAB6B7393F69C8566D3F07B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1884C5B428369DD10604A5FC /* QuartzCore.framework */; };
5557
CB5E07E8C6A42B9D1D2956A5 /* PlaybackDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED5357DF3752411CC0A8B1E /* PlaybackDiagnostics.swift */; };
58+
D483E2EB8422615968CDFAAA /* MediaPreviewPlayerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70C1B6AEE09C5CEE98DA7B2 /* MediaPreviewPlayerSession.swift */; };
5659
E020F82932673E6AAAEF9802 /* VideoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82538038139633F4454BF3B8 /* VideoLayout.swift */; };
5760
E41C8BF1F4FACA82DDD6B11D /* PreviewMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EBFF00EB96786F2F91B23B6 /* PreviewMetadata.swift */; };
5861
E58CE0689863AA40D25FF392 /* VLCVideoLayerHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21ACF4081841AC895F797878 /* VLCVideoLayerHostView.swift */; };
@@ -154,6 +157,7 @@
154157
CED5357DF3752411CC0A8B1E /* PlaybackDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackDiagnostics.swift; sourceTree = "<group>"; };
155158
D74DAEB38D6766046DE5347E /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; };
156159
E3D0BF10236F4D4666B61CC1 /* MKVQuickLookPreviewExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MKVQuickLookPreviewExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
160+
E70C1B6AEE09C5CEE98DA7B2 /* MediaPreviewPlayerSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewPlayerSession.swift; sourceTree = "<group>"; };
157161
E78A20ACD8AECE98CB1DFA53 /* AppStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStatusViewModel.swift; sourceTree = "<group>"; };
158162
F9D991977061BA25D21BFB70 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
159163
/* End PBXFileReference section */
@@ -215,6 +219,7 @@
215219
isa = PBXGroup;
216220
children = (
217221
9555F7EA3CD226932F56DAAE /* MediaPreviewPlayer.swift */,
222+
E70C1B6AEE09C5CEE98DA7B2 /* MediaPreviewPlayerSession.swift */,
218223
B4C0D0E7E3433A3474B1CA13 /* VLCKitMediaPreviewPlayer.swift */,
219224
21ACF4081841AC895F797878 /* VLCVideoLayerHostView.swift */,
220225
);
@@ -424,6 +429,7 @@
424429
buildActionMask = 2147483647;
425430
files = (
426431
C97CB0AA2C6918F5835F1B7B /* MediaPreviewPlayer.swift in Sources */,
432+
D483E2EB8422615968CDFAAA /* MediaPreviewPlayerSession.swift in Sources */,
427433
C6DD68259D9A89D903E9238C /* PlaybackDiagnostics.swift in Sources */,
428434
EC0DB67FA2FA5419279BD272 /* PreviewContentView.swift in Sources */,
429435
E41C8BF1F4FACA82DDD6B11D /* PreviewMetadata.swift in Sources */,
@@ -440,6 +446,7 @@
440446
files = (
441447
EBDBACD66302E999D1234CA4 /* BundleMetadataTests.swift in Sources */,
442448
904DAA5E391982B8C176884E /* MediaPreviewPlayer.swift in Sources */,
449+
04AD3364848C6C9F1181F5D9 /* MediaPreviewPlayerSession.swift in Sources */,
443450
CB5E07E8C6A42B9D1D2956A5 /* PlaybackDiagnostics.swift in Sources */,
444451
2B88CFF90BCF0FB3170CED64 /* PreviewContentView.swift in Sources */,
445452
EF7ADB30C280537CC95076AB /* PreviewMetadata.swift in Sources */,
@@ -458,6 +465,7 @@
458465
1F289B33F3D8F5093D940555 /* MKVQuickLookApp.swift in Sources */,
459466
4F70B1768253CC6D0CC94E8B /* MainWindowView.swift in Sources */,
460467
9E318696532BB1FD7D431823 /* MediaPreviewPlayer.swift in Sources */,
468+
3E4C4A83A07B01771B7AF288 /* MediaPreviewPlayerSession.swift in Sources */,
461469
AB22A85B3CECA21C04A68B5F /* PlaybackDiagnostics.swift in Sources */,
462470
2E90D1CC045B923E1911504B /* PlaybackLabViewController.swift in Sources */,
463471
578CD529E1ABB780CD20B7BF /* PlaybackLabWindowView.swift in Sources */,

MKVQuickLookApp/Resources/Info.plist

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@
7474
<string>com.robertwildling.mkvquicklook.avi</string>
7575
</array>
7676
</dict>
77+
<dict>
78+
<key>CFBundleTypeName</key>
79+
<string>Opus Audio</string>
80+
<key>CFBundleTypeRole</key>
81+
<string>Viewer</string>
82+
<key>LSHandlerRank</key>
83+
<string>Owner</string>
84+
<key>LSItemContentTypes</key>
85+
<array>
86+
<string>com.robertwildling.mkvquicklook.opus</string>
87+
</array>
88+
</dict>
7789
</array>
7890
<key>UTExportedTypeDeclarations</key>
7991
<array>
@@ -172,6 +184,31 @@
172184
</array>
173185
</dict>
174186
</dict>
187+
<dict>
188+
<key>UTTypeConformsTo</key>
189+
<array>
190+
<string>public.audio</string>
191+
<string>public.audiovisual-content</string>
192+
<string>public.data</string>
193+
</array>
194+
<key>UTTypeDescription</key>
195+
<string>Opus Audio</string>
196+
<key>UTTypeIdentifier</key>
197+
<string>com.robertwildling.mkvquicklook.opus</string>
198+
<key>UTTypeTagSpecification</key>
199+
<dict>
200+
<key>public.filename-extension</key>
201+
<array>
202+
<string>opus</string>
203+
</array>
204+
<key>public.mime-type</key>
205+
<array>
206+
<string>audio/opus</string>
207+
<string>audio/ogg</string>
208+
<string>application/ogg</string>
209+
</array>
210+
</dict>
211+
</dict>
175212
</array>
176213
</dict>
177214
</plist>

MKVQuickLookPreviewExtension/Resources/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
<string>org.xiph.ogv</string>
3636
<string>org.videolan.ogg-audio</string>
3737
<string>org.xiph.ogg.vorbis</string>
38+
<string>org.xiph.opus</string>
3839
<string>public.avi</string>
3940
<string>com.robertwildling.mkvquicklook.mkv</string>
4041
<string>com.robertwildling.mkvquicklook.webm</string>
4142
<string>com.robertwildling.mkvquicklook.ogg-video</string>
4243
<string>com.robertwildling.mkvquicklook.avi</string>
44+
<string>com.robertwildling.mkvquicklook.opus</string>
4345
</array>
4446
<key>QLSupportsSearchableItems</key>
4547
<false/>

MKVQuickLookPreviewExtension/Sources/PreviewViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,15 @@ final class PreviewViewController: NSViewController, QLPreviewingController {
7474
self?.previewContentView.setVideoOutputVisible(isVisible)
7575
}
7676

77+
self.player = MediaPreviewPlayerSession.replace(current: self.player, with: player)
78+
7779
await MainActor.run {
7880
title = metadata.displayName
7981
previewContentView.apply(metadata: metadata)
8082
previewContentView.attachRenderView(player.renderView)
8183
}
8284

8385
currentFileURL = url
84-
self.player = player
8586
isMediaLoaded = false
8687
updatePresentationModeAndPlayback()
8788
} catch {

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,28 @@ macOS host app plus Quick Look Preview Extension scaffold for:
77
- `mkv`
88
- `webm`
99
- `ogg` / `ogv`
10+
- `opus` (audio-only)
1011
- `avi` (best-effort)
1112

1213
`MKVQuickLook` provides a Finder Quick Look preview for these formats on macOS by shipping a small host app with a bundled Quick Look Preview Extension and embedded `VLCKit` playback backend.
1314

1415
![MKVQuickLook screenshot](screenshot.png)
1516

17+
## Gatekeeper
18+
19+
This app is currently distributed as an ad hoc signed DMG build and is **not notarized**.
20+
21+
- macOS Gatekeeper may warn when users open it for the first time.
22+
- That is expected for the current release process.
23+
- Users may need to open it via Finder context menu or allow it in System Settings after the first launch attempt.
24+
1625
Current status:
1726

1827
- host app with main window, settings/help UI, and playback lab
1928
- Quick Look Preview Extension registered for owned target file types
2029
- `VLCKit 3.7.2` fetched into `Vendor/` by a bootstrap script
2130
- direct VLCKit-backed playback path for supported files
31+
- audio-only `.opus` files use the same VLCKit backend with an audio preview UI instead of a video frame
2232
- Finder registration and Launch Services ownership wired into the app bundle metadata
2333
- playback defaults to paused in Quick Look
2434
- renderer, metadata, and UI regressions covered by automated tests
@@ -207,6 +217,7 @@ Suggested local contents:
207217

208218
- one or more `.mkv` files, including at least one problematic real-world sample
209219
- one `.webm` file
220+
- one `.opus` audio-only sample
210221
- one `.ogv` or Theora-in-Ogg sample if Ogg video support is being tested
211222
- one `.avi` sample if AVI behavior is being checked
212223

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
@MainActor
4+
enum MediaPreviewPlayerSession {
5+
static func replace(current: MediaPreviewPlayer?, with next: MediaPreviewPlayer?) -> MediaPreviewPlayer? {
6+
if let current, current !== next {
7+
current.stop()
8+
}
9+
return next
10+
}
11+
}

Shared/Sources/PreviewContentView.swift

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ final class PreviewContentView: NSView {
9494
private var currentSeekInteractionID: PlaybackDiagnostics.InteractionID?
9595
private var currentPlaybackState: MediaPreviewPlaybackState = .idle
9696
private var isVideoOutputVisible = false
97+
private var mediaKind: PreviewMediaKind = .video
9798

9899
var isPlaceholderVisibleForTesting: Bool {
99100
!placeholderLabel.isHidden
@@ -107,6 +108,14 @@ final class PreviewContentView: NSView {
107108
playbackButton.isEnabled
108109
}
109110

111+
var isVideoFrameHiddenForTesting: Bool {
112+
videoFrameView.isHidden
113+
}
114+
115+
var isControlsRowHiddenForTesting: Bool {
116+
controlsRow.isHidden
117+
}
118+
110119
override init(frame frameRect: NSRect) {
111120
super.init(frame: frameRect)
112121
configureView()
@@ -124,21 +133,32 @@ final class PreviewContentView: NSView {
124133
}
125134

126135
func apply(metadata: PreviewMetadata) {
136+
setMediaKind(metadata.mediaKind)
127137
iconView.image = metadata.icon
128138
titleLabel.stringValue = metadata.displayName
129139
subtitleLabel.stringValue = "\(metadata.typeDescription) • .\(metadata.fileExtension)"
130-
compactHintLabel.stringValue = "Column preview stays paused. Press Space for the full Quick Look player."
140+
compactHintLabel.stringValue = metadata.mediaKind == .audioOnly
141+
? "Column preview stays paused. Press Space for the full Quick Look audio preview."
142+
: "Column preview stays paused. Press Space for the full Quick Look player."
131143
detailsLabel.stringValue = """
132144
Path: \(metadata.fileURL.path)
133145
Size: \(metadata.fileSizeDescription)
134146
Modified: \(metadata.modifiedDateDescription)
135147
"""
136-
statusLabel.stringValue = "Opening with VLCKit..."
148+
statusLabel.stringValue = metadata.mediaKind == .audioOnly
149+
? "Opening audio with VLCKit..."
150+
: "Opening with VLCKit..."
137151
playbackButton.isEnabled = true
138152
seekSlider.isEnabled = false
139153
volumeSlider.isEnabled = true
140154
}
141155

156+
func setMediaKind(_ mediaKind: PreviewMediaKind) {
157+
self.mediaKind = mediaKind
158+
refreshLayoutForCurrentMode()
159+
refreshPlaceholderVisibility()
160+
}
161+
142162
func applyPlaceholder(title: String, subtitle: String, details: String, status: String, symbolName: String = "play.rectangle") {
143163
iconView.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
144164
titleLabel.stringValue = title
@@ -241,6 +261,8 @@ final class PreviewContentView: NSView {
241261
stackTopConstraint?.constant = 24
242262
stackBottomConstraint?.constant = -24
243263
}
264+
265+
refreshLayoutForCurrentMode()
244266
}
245267

246268
func updatePlaybackMetrics(_ metrics: MediaPreviewPlaybackMetrics) {
@@ -267,7 +289,7 @@ final class PreviewContentView: NSView {
267289
playbackButton.isEnabled = true
268290
placeholderLabel.stringValue = "Ready to play"
269291
case .opening:
270-
statusLabel.stringValue = "Opening media..."
292+
statusLabel.stringValue = mediaKind == .audioOnly ? "Opening audio..." : "Opening media..."
271293
playbackButton.title = "Pause"
272294
playbackButton.isEnabled = true
273295
placeholderLabel.stringValue = "Preparing video surface..."
@@ -277,7 +299,9 @@ final class PreviewContentView: NSView {
277299
playbackButton.isEnabled = true
278300
placeholderLabel.stringValue = "Buffering..."
279301
case .playing:
280-
statusLabel.stringValue = "Playing original file directly through VLCKit."
302+
statusLabel.stringValue = mediaKind == .audioOnly
303+
? "Playing original audio file directly through VLCKit."
304+
: "Playing original file directly through VLCKit."
281305
playbackButton.title = "Pause"
282306
playbackButton.isEnabled = true
283307
case .paused:
@@ -506,7 +530,22 @@ final class PreviewContentView: NSView {
506530
videoCanvasView.frame = VideoLayout.fittedRect(contentSize: videoPresentationSize, in: safeBounds).integral
507531
}
508532

533+
private func refreshLayoutForCurrentMode() {
534+
let isExpanded = compactHintLabel.isHidden
535+
536+
if isExpanded {
537+
let shouldHideVideoFrame = mediaKind == .audioOnly
538+
videoFrameView.isHidden = shouldHideVideoFrame
539+
badgeLabel.isHidden = shouldHideVideoFrame
540+
}
541+
}
542+
509543
private func refreshPlaceholderVisibility() {
544+
guard mediaKind == .video else {
545+
placeholderLabel.isHidden = true
546+
return
547+
}
548+
510549
let shouldHidePlaceholder: Bool
511550

512551
switch currentPlaybackState {

Shared/Sources/PreviewMetadata.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import AppKit
22
import Foundation
33

4+
enum PreviewMediaKind: Equatable {
5+
case video
6+
case audioOnly
7+
}
8+
49
struct PreviewMetadata {
510
let fileURL: URL
611
let displayName: String
712
let fileExtension: String
13+
let mediaKind: PreviewMediaKind
814
let typeDescription: String
915
let fileSizeDescription: String
1016
let modifiedDateDescription: String
@@ -21,12 +27,22 @@ struct PreviewMetadata {
2127
self.fileURL = fileURL
2228
displayName = resourceValues.name ?? fileURL.lastPathComponent
2329
fileExtension = fileURL.pathExtension.lowercased()
30+
mediaKind = Self.mediaKind(for: fileExtension)
2431
typeDescription = Self.typeDescription(for: fileExtension, contentTypeDescription: resourceValues.contentType?.localizedDescription)
2532
fileSizeDescription = Self.fileSizeDescription(bytes: resourceValues.fileSize)
2633
modifiedDateDescription = Self.modifiedDateDescription(resourceValues.contentModificationDate)
2734
icon = NSWorkspace.shared.icon(forFile: fileURL.path)
2835
}
2936

37+
static func mediaKind(for fileExtension: String) -> PreviewMediaKind {
38+
switch fileExtension {
39+
case "opus":
40+
return .audioOnly
41+
default:
42+
return .video
43+
}
44+
}
45+
3046
static func typeDescription(for fileExtension: String, contentTypeDescription: String?) -> String {
3147
if let contentTypeDescription, !contentTypeDescription.isEmpty {
3248
return contentTypeDescription
@@ -41,6 +57,8 @@ struct PreviewMetadata {
4157
return "Ogg Video"
4258
case "avi":
4359
return "AVI Video"
60+
case "opus":
61+
return "Opus Audio"
4462
default:
4563
return "Media File"
4664
}

0 commit comments

Comments
 (0)