Skip to content

Instantly share code, notes, and snippets.

@ken-itakura
Created November 18, 2024 03:46
Show Gist options
  • Save ken-itakura/6be0e0cd00cf11aba4405b3f541ae341 to your computer and use it in GitHub Desktop.
Save ken-itakura/6be0e0cd00cf11aba4405b3f541ae341 to your computer and use it in GitHub Desktop.
Play and control youtube video by SwiftUI controls
//
// 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