Created
November 18, 2024 03:46
-
-
Save ken-itakura/6be0e0cd00cf11aba4405b3f541ae341 to your computer and use it in GitHub Desktop.
Play and control youtube video by SwiftUI controls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// YouTubePlayerView.swift | |
// | |
// Created by Ken Itakura on 2024/11/16. | |
// | |
import SwiftUI | |
import WebKit | |
struct YouTubePlayerView: UIViewRepresentable { | |
let videoId: String | |
@Binding var webView: WKWebView? | |
@Binding var isPlaying: Bool | |
@Binding var currentTime: Double | |
@Binding var duration: Double | |
func makeUIView(context: Context) -> WKWebView { | |
let contentController = WKUserContentController() | |
// Message handlers | |
contentController.add(context.coordinator, name: "isPlaying") | |
contentController.add(context.coordinator, name: "currentTime") | |
contentController.add(context.coordinator, name: "duration") | |
let configuration = WKWebViewConfiguration() | |
configuration.allowsInlineMediaPlayback = true // Allow inline play | |
configuration.mediaTypesRequiringUserActionForPlayback = [] // Allow auto play | |
configuration.userContentController = contentController | |
let newWebView = WKWebView(frame: .zero, configuration: configuration) | |
newWebView.navigationDelegate = context.coordinator | |
#if DEBUG | |
newWebView.isInspectable = true // enable safari inspector tool | |
#endif | |
DispatchQueue.main.async { | |
self.webView = newWebView | |
} | |
let injectingScript = """ | |
let player | |
function onYouTubeIframeAPIReady() { | |
console.log('YouTubeIframeAPIReady event fired!'); | |
player = new YT.Player('player', { | |
videoId: '\(videoId)', | |
// IFrame Player API Parameters https://developers.google.com/youtube/player_parameters?hl=ja#Parameters | |
playerVars: { | |
'playsinline': 1, // enable inline play. allowsInlineMediaPlayback must set for UIWebViews | |
'enablejsapi': 1, // enable IFrame Player API | |
'controls': 0, // Hide controls | |
'rel': 0, // Do't display related medias | |
'autoplay': 0 // Don't auto play | |
}, | |
events: { | |
'onReady': onPlayerReady, | |
'onStateChange': onPlayerStateChange | |
} | |
}); | |
} | |
function onPlayerReady(event) { | |
// event.target.playVideo(); // Auto start | |
setInterval(function(){ | |
window.webkit.messageHandlers.currentTime.postMessage(player.getCurrentTime()); | |
window.webkit.messageHandlers.duration.postMessage(player.getDuration()); | |
}, 500); | |
} | |
function onPlayerStateChange(event) { | |
if (event.data == YT.PlayerState.PLAYING) { | |
window.webkit.messageHandlers.isPlaying.postMessage(true); | |
} else if (event.data == YT.PlayerState.PAUSED) { | |
window.webkit.messageHandlers.isPlaying.postMessage(false); | |
} | |
} | |
// IFrame Player API Functions https://developers.google.com/youtube/iframe_api_reference?hl=ja#Functions | |
window.playVideo = function() { | |
console.log('Start Pressed'); | |
player.playVideo(); | |
} | |
window.pauseVideo = function() { | |
console.log('Pause Pressed'); | |
player.pauseVideo(); | |
} | |
window.seekTo = function(params) { | |
console.log('Seek to ' + params.position); | |
player.seekTo(Number(params.position), true); | |
} | |
""" | |
let pageHtml = """ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Youtube iframe</title> | |
<script type="text/javascript" src="https://www.youtube.com/iframe_api"></script> | |
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<style> | |
body, html { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
background-color: black; | |
} | |
iframe { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
border: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="player"></div> | |
<script>\(injectingScript)</script> | |
</body> | |
</html> | |
""" | |
newWebView.loadHTMLString(pageHtml, baseURL: nil) | |
return newWebView | |
} | |
func updateUIView(_ uiView: WKWebView, context: Context) {} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(self) | |
} | |
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { | |
var parent: YouTubePlayerView | |
init(_ parent: YouTubePlayerView) { | |
self.parent = parent | |
} | |
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { | |
switch message.name { | |
case "isPlaying": | |
parent.isPlaying = message.body as! Bool | |
case "currentTime": | |
parent.currentTime = message.body as! Double | |
case "duration": | |
parent.duration = message.body as! Double | |
default: | |
break | |
} | |
} | |
} | |
} | |
struct CustomYouTubePlayerView: View { | |
let videoId: String | |
@State private var isPlaying = false | |
@State private var currentTime: Double = 0.0 | |
@State private var duration: Double = 0.0 | |
@State private var seekTime: Double = 0.0 | |
@State private var webView: WKWebView? = nil | |
@State private var isUserManipulateSlider = false | |
@State private var sliderPosition: Double = 0.0 | |
var body: some View { | |
VStack { | |
YouTubePlayerView(videoId: videoId, webView: $webView, isPlaying: $isPlaying, currentTime: $currentTime, duration: $duration) | |
.frame(width: 300, height: 200) | |
.onChange(of: currentTime, perform: {newValue in | |
if(!isUserManipulateSlider){ | |
sliderPosition = Double(newValue) | |
} | |
}) | |
VStack { | |
Button(action: { | |
guard let webView = webView else { | |
print("WebView is not ready") | |
return | |
} | |
if isPlaying { | |
webView.evaluateJavaScript("pauseVideo()"){ result, error in | |
if let error = error { | |
print("Error calling JavaScript function: \(error)") | |
} | |
} | |
} else { | |
webView.evaluateJavaScript("playVideo()"){ result, error in | |
if let error = error { | |
print("Error calling JavaScript function: \(error)") | |
} | |
} | |
} | |
}) { | |
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill") | |
.font(.system(size: 40)) | |
} | |
Slider(value: $sliderPosition, in: 0...duration, onEditingChanged: { editing in | |
isUserManipulateSlider = editing | |
guard let webView = webView else { | |
print("WebView is not ready") | |
return | |
} | |
if !editing { | |
webView.evaluateJavaScript("seekTo({position: \(sliderPosition)})"){ result, error in | |
if let error = error { | |
print("Error calling JavaScript function: \(error)") | |
} | |
} | |
} | |
}) | |
.padding() | |
} | |
} | |
} | |
} | |
#Preview { | |
CustomYouTubePlayerView(videoId: "dFf4AgBNR1E") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment