I used SwiftUI to create a Video Player, which loads a video using an imagePickerController, and then it is suppose to play the video once retrieved from the device. I found that the Video Player was not refreshing after retrieving the video. I am not sure how to give the appropriate @State|@Binding which is necessary to refresh it.
I learned how to code a Video Player using available online resources. And I have found a way to load a video from my device and load it to my video player. However, when I press the play button, after I have loaded the video, only the sound was played. I have tried to make the video player @State|@Binding but cannot find the solution as it does not appear to be intuitively done so.
Can anyone suggest how to update my code for the Video Player using SwiftUI?
P.S. 1) You must use an actual device to load video; and 2) The slider does not work yet. I will work on that next.
Disclosure:
I have adapted this code from online resources. The original source code for the majority of this work can be found at these links:
How to open the ImagePicker in SwiftUI?
https://www.raywenderlich.com/5135-how-to-play-record-and-merge-videos-in-ios-and-swift
https://medium.com/@chris.mash/avplayer-swiftui-part-2-player-controls-c28b721e7e27
import SwiftUI
import AVKit
import PhotosUI
import MobileCoreServices
struct ContentView: View {
@State var showImagePicker: Bool = false
@State var url: URL?
var body: some View {
ZStack {
VStack {
Button(action: {
withAnimation {
self.showImagePicker.toggle()
}
}) {
Text("Show image picker")
}
// The video player will needs to be a @State??? as it is not updated with UIView changes but works when no view changes occur.
PlayerContainerView(player: AVPlayer(url: url ?? URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!))
}
if (showImagePicker) {
ImagePicker(isShown: $showImagePicker, url: $url)
}
}
}
}
struct PlayerView: UIViewRepresentable {
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(player: player)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(player: AVPlayer) {
super.init(frame: .zero)
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
struct PlayerContainerView : View {
@State var seekPos = 0.0
private let player: AVPlayer
init(player: AVPlayer) {
self.player = player
}
var body: some View {
VStack {
PlayerView(player: player)
PlayerControlsView(player: player)
}
}
}
struct PlayerControlsView : View {
@State var playerPaused = true
@State var seekPos = 0.0
let player: AVPlayer
var body: some View {
HStack {
Button(action: {
self.playerPaused.toggle()
if self.playerPaused {
self.player.pause()
}
else {
self.player.play()
}
}) {
Image(systemName: playerPaused ? "play" : "pause")
.padding(.leading, 20)
.padding(.trailing, 20)
}
Slider(value: $seekPos, from: 0, through: 1, onEditingChanged: { _ in
guard let item = self.player.currentItem else {
return
}
let targetTime = self.seekPos * item.duration.seconds
self.player.seek(to: CMTime(seconds: targetTime, preferredTimescale: 600))
})
.padding(.trailing, 20)
}
}
}
struct ImagePicker: UIViewControllerRepresentable {
@Binding var isShown: Bool
@Binding var url: URL?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var isShown: Bool
@Binding var url: URL?
init(isShown: Binding<Bool>, url: Binding<URL?>) {
$isShown = isShown
$url = url
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let info = convertFromUIImagePickerControllerInfoKeyDictionary(info)
guard let mediaType = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaType)] as? String,
mediaType == (kUTTypeMovie as String),
let uiURL = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaURL)] as? URL
else { return }
url = uiURL
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, url: $url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.mediaTypes = [kUTTypeMovie as String]
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
fileprivate func convertFromUIImagePickerControllerInfoKeyDictionary(_ input: [UIImagePickerController.InfoKey: Any]) -> [String: Any] {
return Dictionary(uniqueKeysWithValues: input.map {key, value in (key.rawValue, value)})
}
fileprivate func convertFromUIImagePickerControllerInfoKey(_ input: UIImagePickerController.InfoKey) -> String {
return input.rawValue
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(showImagePicker: true)
}
}
#endif
The video player does not play the video as expected, it only played the sound which indicates to me that it is playing the video; however, I cannot see it being played. It remains a black box.
UPDATE: The following is the fully edited code that works as expected (except for the slider), which was answered in comments below:
import SwiftUI
import AVKit
import PhotosUI
import MobileCoreServices
struct ContentView: View {
@State var showImagePicker: Bool = false
@State var url: URL?
var body: some View {
ZStack {
VStack {
Button(action: {
withAnimation {
self.showImagePicker.toggle()
}
}) {
Text("Show image picker")
}
PlayerContainerView(player: AVPlayer(url: url ?? URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!))
}
if (showImagePicker) {
ImagePicker(isShown: $showImagePicker, url: $url)
}
}
}
}
struct PlayerView: UIViewRepresentable {
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
(uiView as? PlayerUIView)?.updatePlayer(player: player)
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(player: player)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(player: AVPlayer) {
super.init(frame: .zero)
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func updatePlayer(player: AVPlayer) {
self.playerLayer.player = player
}
}
struct PlayerContainerView : View {
@State var seekPos = 0.0
private let player: AVPlayer
init(player: AVPlayer) {
self.player = player
}
var body: some View {
VStack {
PlayerView(player: player)
PlayerControlsView(player: player)
}
}
}
struct PlayerControlsView : View {
@State var playerPaused = true
@State var seekPos = 0.0
let player: AVPlayer
var body: some View {
HStack {
Button(action: {
self.playerPaused.toggle()
if self.playerPaused {
self.player.pause()
}
else {
self.player.play()
}
}) {
Image(systemName: playerPaused ? "play" : "pause")
.padding(.leading, 20)
.padding(.trailing, 20)
}
Slider(value: $seekPos, from: 0, through: 1, onEditingChanged: { _ in
guard let item = self.player.currentItem else {
return
}
let targetTime = self.seekPos * item.duration.seconds
self.player.seek(to: CMTime(seconds: targetTime, preferredTimescale: 600))
})
.padding(.trailing, 20)
}
}
}
struct ImagePicker: UIViewControllerRepresentable {
@Binding var isShown: Bool
@Binding var url: URL?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var isShown: Bool
@Binding var url: URL?
init(isShown: Binding<Bool>, url: Binding<URL?>) {
_isShown = isShown
_url = url
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let info = convertFromUIImagePickerControllerInfoKeyDictionary(info)
guard let mediaType = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaType)] as? String,
mediaType == (kUTTypeMovie as String),
let uiURL = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaURL)] as? URL
else { return }
url = uiURL
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, url: $url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.mediaTypes = [kUTTypeMovie as String]
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
fileprivate func convertFromUIImagePickerControllerInfoKeyDictionary(_ input: [UIImagePickerController.InfoKey: Any]) -> [String: Any] {
return Dictionary(uniqueKeysWithValues: input.map {key, value in (key.rawValue, value)})
}
fileprivate func convertFromUIImagePickerControllerInfoKey(_ input: UIImagePickerController.InfoKey) -> String {
return input.rawValue
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(showImagePicker: true)
}
}
#endif
You left your updateUIView empty. You should implement it:
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
(uiView as? PlayerUIView)?.updatePlayer(player: player)
}
And also add the following method to your PlayerUIView:
func updatePlayer(player: AVPlayer) {
self.playerLayer.player = player
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With