Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native video player support #69

Merged
merged 9 commits into from Dec 23, 2022
45 changes: 33 additions & 12 deletions NativeYoutube.xcodeproj/project.pbxproj
Expand Up @@ -22,7 +22,9 @@
13FA3CF127AF820D005555C3 /* VideoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FA3CF027AF820C005555C3 /* VideoRowView.swift */; };
13FA3CF327AF82B2005555C3 /* VideoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FA3CF227AF82B2005555C3 /* VideoListView.swift */; };
13FA3CF527AF8B6B005555C3 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FA3CF427AF8B6B005555C3 /* Request.swift */; };
C22D70B829557E71000DE71E /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = C22D70B729557E71000DE71E /* SDWebImageSwiftUI */; };
70A3843C2955B35E000941F5 /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A3843B2955B35E000941F5 /* Enums.swift */; };
70A3843F2955D1A1000941F5 /* YouTubeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 70A3843E2955D1A1000941F5 /* YouTubeKit */; };
70A3844529561851000941F5 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 70A3844429561851000941F5 /* SDWebImageSwiftUI */; };
C27DD350272C853E00B4DC16 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27DD34F272C853E00B4DC16 /* ContentView.swift */; };
C27DD352272C853F00B4DC16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C27DD351272C853F00B4DC16 /* Assets.xcassets */; };
C27DD355272C853F00B4DC16 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C27DD354272C853F00B4DC16 /* Preview Assets.xcassets */; };
Expand Down Expand Up @@ -65,6 +67,7 @@
13FA3CF027AF820C005555C3 /* VideoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRowView.swift; sourceTree = "<group>"; };
13FA3CF227AF82B2005555C3 /* VideoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoListView.swift; sourceTree = "<group>"; };
13FA3CF427AF8B6B005555C3 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
70A3843B2955B35E000941F5 /* Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = "<group>"; };
C27DD34A272C853E00B4DC16 /* NativeYoutube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NativeYoutube.app; sourceTree = BUILT_PRODUCTS_DIR; };
C27DD34F272C853E00B4DC16 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
C27DD351272C853F00B4DC16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -97,9 +100,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C22D70B829557E71000DE71E /* SDWebImageSwiftUI in Frameworks */,
70A3844529561851000941F5 /* SDWebImageSwiftUI in Frameworks */,
C2A7FC23274D78F0000D6D33 /* YouTubePlayerKit in Frameworks */,
C27DD38E272C93AD00B4DC16 /* SwiftyJSON in Frameworks */,
70A3843F2955D1A1000941F5 /* YouTubeKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -195,6 +199,7 @@
children = (
131A386C27AF191200B67A1B /* VideoModel.swift */,
C27DD385272C8DD100B4DC16 /* Constants.swift */,
70A3843B2955B35E000941F5 /* Enums.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -275,7 +280,8 @@
packageProductDependencies = (
C27DD38D272C93AD00B4DC16 /* SwiftyJSON */,
C2A7FC22274D78F0000D6D33 /* YouTubePlayerKit */,
C22D70B729557E71000DE71E /* SDWebImageSwiftUI */,
70A3843E2955D1A1000941F5 /* YouTubeKit */,
70A3844429561851000941F5 /* SDWebImageSwiftUI */,
);
productName = NativeYoutube;
productReference = C27DD34A272C853E00B4DC16 /* NativeYoutube.app */;
Expand Down Expand Up @@ -308,7 +314,8 @@
packageReferences = (
C27DD38C272C93AD00B4DC16 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
C2A7FC21274D78F0000D6D33 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */,
C22D70B629557E71000DE71E /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
70A3843D2955D1A1000941F5 /* XCRemoteSwiftPackageReference "YouTubeKit" */,
70A3844329561850000941F5 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
);
productRefGroup = C27DD34B272C853E00B4DC16 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -370,6 +377,7 @@
133DCE5627ADF29C00AE6F1F /* YoutubePreferenceView.swift in Sources */,
C2A7FC25274D791D000D6D33 /* VideoPlayerView.swift in Sources */,
13FA3CF127AF820D005555C3 /* VideoRowView.swift in Sources */,
70A3843C2955B35E000941F5 /* Enums.swift in Sources */,
131A386D27AF191200B67A1B /* VideoModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -506,7 +514,7 @@
CURRENT_PROJECT_VERSION = 10;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"NativeYoutube/Preview Content\"";
DEVELOPMENT_TEAM = 4538W4A79B;
Aayush9029 marked this conversation as resolved.
Show resolved Hide resolved
DEVELOPMENT_TEAM = 2CPY2SAJ3X;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down Expand Up @@ -539,7 +547,7 @@
CURRENT_PROJECT_VERSION = 10;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"NativeYoutube/Preview Content\"";
DEVELOPMENT_TEAM = 4538W4A79B;
DEVELOPMENT_TEAM = 2CPY2SAJ3X;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down Expand Up @@ -584,12 +592,20 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
C22D70B629557E71000DE71E /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
70A3843D2955D1A1000941F5 /* XCRemoteSwiftPackageReference "YouTubeKit" */ = {
Aayush9029 marked this conversation as resolved.
Show resolved Hide resolved
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
repositoryURL = "https://github.com/alexeichhorn/YouTubeKit";
requirement = {
branch = master;
kind = branch;
kind = upToNextMajorVersion;
minimumVersion = 0.1.6;
};
};
70A3844329561850000941F5 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
C27DD38C272C93AD00B4DC16 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
Expand All @@ -611,9 +627,14 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
C22D70B729557E71000DE71E /* SDWebImageSwiftUI */ = {
70A3843E2955D1A1000941F5 /* YouTubeKit */ = {
isa = XCSwiftPackageProductDependency;
package = 70A3843D2955D1A1000941F5 /* XCRemoteSwiftPackageReference "YouTubeKit" */;
productName = YouTubeKit;
};
70A3844429561851000941F5 /* SDWebImageSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = C22D70B629557E71000DE71E /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
package = 70A3844329561850000941F5 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
productName = SDWebImageSwiftUI;
};
C27DD38D272C93AD00B4DC16 /* SwiftyJSON */ = {
Expand Down
Expand Up @@ -12,10 +12,10 @@
{
"identity" : "sdwebimageswiftui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI",
"state" : {
"branch" : "master",
"revision" : "ed288667c909c89127ab1b690113a3e397af3098"
"revision" : "ed288667c909c89127ab1b690113a3e397af3098",
"version" : "2.2.1"
}
},
{
Expand All @@ -27,6 +27,15 @@
"version" : "4.3.0"
}
},
{
"identity" : "youtubekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/alexeichhorn/YouTubeKit",
"state" : {
"revision" : "7dcbb57b39a58dd5c208c40665383de0934e1ce7",
"version" : "0.1.6"
}
},
{
"identity" : "youtubeplayerkit",
"kind" : "remoteSourceControl",
Expand Down
3 changes: 2 additions & 1 deletion NativeYoutube/Extensions/View+Extension.swift
Expand Up @@ -11,7 +11,7 @@ import SwiftUI
extension View {
private func newWindowInternal(with title: String, isTransparent: Bool = false) -> NSWindow {
let window = KeyWindow(
contentRect: NSRect(x: 20, y: 20, width: 640, height: 360),
contentRect: NSRect(x: 20, y: 20, width: 480, height: 270),
styleMask: [.titled, .closable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
Expand All @@ -22,6 +22,7 @@ extension View {
window.title = title
window.makeKeyAndOrderFront(self)
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
// Failed attempt to make the window sticks at the right aspect ratio, keeping it in comment as it should work (see https://developer.apple.com/documentation/appkit/nswindow/1419507-aspectratio)
// window.aspectRatio = NSMakeSize(16.0, 9.0)
if isTransparent {
Expand Down
2 changes: 2 additions & 0 deletions NativeYoutube/Models/Constants.swift
Expand Up @@ -30,6 +30,8 @@ enum StatusStates: String {
enum AppStorageStrings: String {
case apiKey = "Api Key"
case playListID = "Playlist ID for your goto playlist"
case videoClickBehaviour = "Behaviour when double click on a video"
Aayush9029 marked this conversation as resolved.
Show resolved Hide resolved
case useNativePlayer = "Use Native player"

// iina plus works much better, this could potentially replace youtubeplayerkit
case useIINA = "Use IINA Plus"
Expand Down
18 changes: 18 additions & 0 deletions NativeYoutube/Models/Enums.swift
@@ -0,0 +1,18 @@
//
// Enums.swift
// NativeYoutube
//
// Created by Adélaïde Sky on 23/12/2022.
//

import Foundation

// Didn't knew where to put it so i created this file lol

// All behaviours available on double clicking a video element.
enum VideoClickBehaviour: String, CaseIterable {
case nothing
case playVideo
case openOnYoutube
case playInIINA
}
10 changes: 1 addition & 9 deletions NativeYoutube/NativeYoutubeApp.swift
Expand Up @@ -13,21 +13,13 @@ struct NativeYoutubeApp: App {
@StateObject private var youtubePlayerViewModel = YoutubePlayerViewModel()
@StateObject private var searchViewModel = SearchViewModel()
var body: some Scene {
MenuBarExtra("Native Youtube", systemImage: "play.circle") {
MenuBarExtra("Native Youtube", systemImage: "play.rectangle.fill") {
ContentView()
.frame(width: 360, height: 512)
.environmentObject(appStateViewModel)
.environmentObject(youtubePlayerViewModel)
.environmentObject(searchViewModel)
}
.menuBarExtraStyle(WindowMenuBarExtraStyle())

Settings {
PreferencesView()
.padding(-12)
.frame(minWidth: 320, minHeight: 512)
.environmentObject(appStateViewModel)
}
.windowStyle(.hiddenTitleBar)
}
}
2 changes: 2 additions & 0 deletions NativeYoutube/ViewModels/AppStateViewModel.swift
Expand Up @@ -14,6 +14,8 @@ class AppStateViewModel: ObservableObject {
@AppStorage(AppStorageStrings.apiKey.rawValue) var apiKey = Constants.defaultAPIKey
@AppStorage(AppStorageStrings.playListID.rawValue) var playListID = Constants.defaultPlaylistID
@AppStorage(AppStorageStrings.useIINA.rawValue) var useIINA: Bool = false
@AppStorage(AppStorageStrings.videoClickBehaviour.rawValue) var vidClickBehaviour: VideoClickBehaviour = .playVideo
@AppStorage(AppStorageStrings.useNativePlayer.rawValue) var useNativePlayer: Bool = true

// MARK: States

Expand Down
7 changes: 4 additions & 3 deletions NativeYoutube/ViewModels/YoutubePlayerViewModel.swift
Expand Up @@ -9,10 +9,11 @@ import SwiftUI
import YouTubePlayerKit

class YoutubePlayerViewModel: ObservableObject {
func playVideo(url: String) {
let videoPlayer = YouTubePlayer(source: YouTubePlayer.Source.url(url), configuration: YoutubePlayerViewModel.configuration)
PopupPlayerView(youtubePlayer: videoPlayer)
func playVideo(url: URL, appState: AppStateViewModel) {
let videoPlayer = YouTubePlayer(source: YouTubePlayer.Source.url(url.absoluteString), configuration: YoutubePlayerViewModel.configuration)
PopupPlayerView(appStateViewModel: appState, youtubePlayer: videoPlayer, videoURL: url)
.openNewWindow(isTransparent: true)

}

static let configuration = YouTubePlayer.Configuration(
Expand Down
14 changes: 14 additions & 0 deletions NativeYoutube/Views/PreferencesView/GeneralPreferenceView.swift
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import YouTubeKit

struct GeneralPreferenceView: View {
@EnvironmentObject var appStateViewModel: AppStateViewModel
Expand All @@ -24,6 +25,19 @@ struct GeneralPreferenceView: View {
Toggle("Use IINA", isOn: $appStateViewModel.useIINA)
.toggleStyle(.switch)
.bold()

Toggle("Use native player", isOn: $appStateViewModel.useNativePlayer)
.toggleStyle(.switch)
.bold()

Picker("When double click on a video...", selection: $appStateViewModel.vidClickBehaviour) {
Text("Do nothing").tag(VideoClickBehaviour.nothing)
Text("Play video").tag(VideoClickBehaviour.playVideo)
Text("Open on YouTube").tag(VideoClickBehaviour.openOnYoutube)
if appStateViewModel.useIINA {
Text("Play video in IINA").tag(VideoClickBehaviour.playInIINA)
}
}
}
.padding()
.background(.ultraThinMaterial)
Expand Down
58 changes: 47 additions & 11 deletions NativeYoutube/Views/SharedViews/PopupPlayerView.swift
Expand Up @@ -7,41 +7,77 @@

import SwiftUI
import YouTubePlayerKit
import AVKit
import YouTubeKit

struct PopupPlayerView: View {
//As view is displayed via a function, can't use EnvironnementObject -> Re-declaring useNativePlayer var in the view.
@ObservedObject var appStateViewModel: AppStateViewModel
@StateObject var youtubePlayer: YouTubePlayer

let videoURL: URL
@State var player: AVPlayer? = nil
@State var isHoveringOnPlayer = false

var body: some View {
ZStack(alignment: .topLeading) {

VStack {
ZStack {
Rectangle()
.foregroundColor(.clear)
VideoPlayerView(youtubePlayer: youtubePlayer)
}
if isHoveringOnPlayer {
VideoPlayerControlsView(viewModel: .init(youtubePlayer: youtubePlayer))
.padding(.horizontal)
.padding(.bottom, 5)
if appStateViewModel.useNativePlayer {
if player != nil {
ZStack {
VideoPlayer(player: player)
}
} else {
ProgressView().frame(width: 600, height: 400)
}
} else {
ZStack {
Rectangle()
.foregroundColor(.clear)
VideoPlayerView(youtubePlayer: youtubePlayer)
}
if isHoveringOnPlayer {
VideoPlayerControlsView(viewModel: .init(youtubePlayer: youtubePlayer))
.padding(.horizontal)
.padding(.bottom, 5)
}
}


}

if isHoveringOnPlayer {
PopUpPlayerCloseButton()
.onTapGesture {
appStateViewModel.togglePlaying("")
NSApp.keyWindow?.close()
}
}
}
.task {
if appStateViewModel.useNativePlayer {
//We need to get the video's stream in async, so we set a task who runs when view appears to set the value of the player to the video stream.
let video = YouTube(url: videoURL)
do {
let streams = try await video.streams
//Even though it should return the highest resolution stream for the video, as AVPlayer doesn't support DASH streams, it returns a not-that-great quality stream
player = AVPlayer(url: streams
.filter { $0.isProgressive }
.highestResolutionStream()!.url)
// we need to start playback as it doesn't play automatically
player!.play()
} catch {}
}
}
.onHover { hovering in
withAnimation {
isHoveringOnPlayer = hovering
}
}
.background(VisualEffectView(material: .popover, blendingMode: .behindWindow))
.cornerRadius(10)
.frame(minWidth: 480, minHeight: 270)
.frame(minWidth: 320, minHeight: 180)
}
}

Expand All @@ -63,6 +99,6 @@ struct PopUpPlayerCloseButton: View {

struct PopupPlayerView_Previews: PreviewProvider {
static var previews: some View {
PopupPlayerView(youtubePlayer: YoutubePlayerViewModel.exampleVideo)
PopupPlayerView(appStateViewModel: AppStateViewModel(), youtubePlayer: YoutubePlayerViewModel.exampleVideo, videoURL: URL(string: "https://www.youtube.com/watch?v=EgBJmlPo8Xw")!)
}
}
2 changes: 1 addition & 1 deletion NativeYoutube/Views/SharedViews/VideoContextMenuView.swift
Expand Up @@ -28,7 +28,7 @@ struct VideoContextMenuView: View {
VStack {
Button {
appStateViewModel.togglePlaying(video.title)
youtubePlayerViewModel.playVideo(url: video.url.absoluteString)
youtubePlayerViewModel.playVideo(url: video.url, appState: appStateViewModel)
} label: {
Label("Play Video", systemImage: "play.circle")
}
Expand Down