Browse Source

Swift Video

master
marcoschmickler 4 years ago
parent
commit
0f4aafc448
  1. 12
      kplayer.xcodeproj/project.pbxproj
  2. 3
      kplayer/core/KSettings.swift
  3. 1
      kplayer/core/MediaItem.swift
  4. 13
      kplayer/detail/DetailViewController.swift
  5. 3
      kplayer/master/KSettingsView.swift
  6. 103
      kplayer/svideo/SVideoPlayer.swift
  7. 170
      kplayer/svideo/VideoPlayerView.swift
  8. 51
      kplayer/util/AsyncImage.swift
  9. 30
      kplayer/util/Utility.swift

12
kplayer.xcodeproj/project.pbxproj

@ -16,6 +16,7 @@
1C7361D2B6E0AE689FAAF4F4 /* VideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736C7FFBDAC665AE04CB65 /* VideoController.swift */; };
1C7361D3BA77C40275F89D4A /* TimelineMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7368C7B946BC9E067D37E7 /* TimelineMeasure.swift */; };
1C7361F376DA11F17CD3250B /* TrimView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736ABA0E14A51ACAC84AB5 /* TrimView.swift */; };
1C7362AF931E0F228E5D2AED /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360B53C4C1496320953C2 /* VideoPlayerView.swift */; };
1C73631EACF56BABD3B2BCFB /* LayoutTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736BC4450890C45F8FBC63 /* LayoutTools.swift */; };
1C73633AAF0D77F8AC3557B9 /* SVideoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7362603E8588B4D1A8C617 /* SVideoModel.swift */; };
1C73635138BBD2BB480A308F /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C736777456388CA571DA17B /* MediaPlayer.framework */; };
@ -43,6 +44,7 @@
1C73693A1334A7792856FC58 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73611D226B48C24DB37535 /* MasterViewController.swift */; };
1C736953BDBBAFC40884132A /* BrowserController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73602350ACE2436736F981 /* BrowserController.swift */; };
1C73696E4C0353053BF98031 /* links.html in Resources */ = {isa = PBXBuildFile; fileRef = 1C73615FFA2AA98BD1C56CD4 /* links.html */; };
1C736998044A9A7D89411892 /* AsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360B6D0757D4FB6433E7B /* AsyncImage.swift */; };
1C7369ABC44CFB530EA71FB6 /* HeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736D9BB5498E7E8F11C754 /* HeaderCell.swift */; };
1C736A06A2AD75B8C14EEBBE /* HtmlParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */; };
1C736A5FA5BA53B2597F2ED7 /* Kirschkeks-256x256.png in Resources */ = {isa = PBXBuildFile; fileRef = 1C736059262A57AADE6AB761 /* Kirschkeks-256x256.png */; };
@ -56,6 +58,7 @@
1C736D24B49451141CD4B64D /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */; };
1C736D895B75BDCDB35937C1 /* BMTimeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360AE55EB115762C42EB9 /* BMTimeSlider.swift */; };
1C736DB41BD06D359E6A0DEE /* BMSubtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7366AAB82A46086690E164 /* BMSubtitles.swift */; };
1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736677D4EF2437358B2387 /* Utility.swift */; };
1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B41C6AC33F3FA592C63 /* MediaModel.swift */; };
1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; };
1C736ECAE78F5C722423D7ED /* TimelineScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736362946D7A8585B0D875 /* TimelineScroller.swift */; };
@ -93,6 +96,8 @@
1C7360744ABACC3557D05760 /* HanekeFetchOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HanekeFetchOperation.swift; sourceTree = "<group>"; };
1C7360A94DBECA685ED8602F /* ImageLoadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoadOperation.swift; sourceTree = "<group>"; };
1C7360AE55EB115762C42EB9 /* BMTimeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMTimeSlider.swift; sourceTree = "<group>"; };
1C7360B53C4C1496320953C2 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VideoPlayerView.swift; path = svideo/VideoPlayerView.swift; sourceTree = "<group>"; };
1C7360B6D0757D4FB6433E7B /* AsyncImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncImage.swift; sourceTree = "<group>"; };
1C7360D6580FB5D09C2BBCCB /* BMPlayerManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerManager.swift; sourceTree = "<group>"; };
1C73610B997EBA367C806C1B /* BMPlayerCompositionResourceDefinition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerCompositionResourceDefinition.swift; sourceTree = "<group>"; };
1C73611D226B48C24DB37535 /* MasterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = "<group>"; };
@ -117,6 +122,7 @@
1C7365B06FA66294E99AC2D3 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
1C7365F45D765A218FFC100F /* BMPlayerProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerProtocols.swift; sourceTree = "<group>"; };
1C73661561AD069C92FE3B15 /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineView.swift; path = timeline/TimelineView.swift; sourceTree = "<group>"; };
1C736677D4EF2437358B2387 /* Utility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
1C7366AAB82A46086690E164 /* BMSubtitles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMSubtitles.swift; sourceTree = "<group>"; };
1C7366C09381DC0052B52B69 /* EditItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditItemView.swift; sourceTree = "<group>"; };
1C7366D766CDE0C9872E86F5 /* BMPlayerLayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerLayerView.swift; sourceTree = "<group>"; };
@ -239,6 +245,8 @@
1C73648CEC974A2500172064 /* ViewControllerExtensions.swift */,
1C7367ECBD369A2A0C94C499 /* FileHelper.swift */,
1C7364709899FF62774B0199 /* VideoHelper.swift */,
1C736677D4EF2437358B2387 /* Utility.swift */,
1C7360B6D0757D4FB6433E7B /* AsyncImage.swift */,
);
path = util;
sourceTree = "<group>";
@ -344,6 +352,7 @@
1C736ABA0E14A51ACAC84AB5 /* TrimView.swift */,
1C73624617102E0DEB001C25 /* SVideoPlayer.swift */,
1C7362603E8588B4D1A8C617 /* SVideoModel.swift */,
1C7360B53C4C1496320953C2 /* VideoPlayerView.swift */,
);
path = kplayer;
sourceTree = "<group>";
@ -596,6 +605,9 @@
1C736A78C1F8F41E2AEEF278 /* KVideoPlayer.swift in Sources */,
1C736FF8FF423F01F880F94D /* SVideoPlayer.swift in Sources */,
1C73633AAF0D77F8AC3557B9 /* SVideoModel.swift in Sources */,
1C7362AF931E0F228E5D2AED /* VideoPlayerView.swift in Sources */,
1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */,
1C736998044A9A7D89411892 /* AsyncImage.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

3
kplayer/core/KSettings.swift

@ -15,6 +15,9 @@ class KSettings: ObservableObject {
@Published
var edit = false
@Published
var newPlayer = true
convenience init(model: KSettingsModel) {
self.init()
scale = model.scale

1
kplayer/core/MediaItem.swift

@ -37,6 +37,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
var sortName = ""
// Nutzinhalt
@Published
var image: UIImage?
// let didChange = PassthroughSubject<MediaItem,Never>()

13
kplayer/detail/DetailViewController.swift

@ -31,8 +31,6 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
var showFavoritesOnly = false
var collectionView: UICollectionView!
var videoplayer = true
var currentItem: MediaItem?
var defaultItemSize = CGSize(width: (15 * 16) - 6, height: 15 * 9)
@ -93,9 +91,8 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
let settingsButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(settings));
let overviewButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(overview));
let favButton = UIBarButtonItem(barButtonSystemItem: .bookmarks, target: self, action: #selector(favorites));
let vidButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(vplayer));
let browserButton = UIBarButtonItem(barButtonSystemItem: .organize, target: self, action: #selector(fileBrowser));
navigationItem.rightBarButtonItems = [settingsButton, vidButton, favButton, overviewButton,browserButton]
navigationItem.rightBarButtonItems = [settingsButton, favButton, overviewButton,browserButton]
if detailItem != nil {
print("Details \(detailItem!.children)")
}
@ -147,10 +144,6 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
present(fileBrowser, animated: true, completion: nil)
}
@objc func vplayer() {
videoplayer = !videoplayer
}
@objc func favorites() {
showFavoritesOnly = !showFavoritesOnly
collectionView.reloadData()
@ -469,8 +462,8 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
func showVideo(selectedItem: MediaItem) {
var se = selectedItem
var children = detailItem!.children
if videoplayer {
var children = selectedItem.parent!.children
if delegate!.settings().newPlayer {
let model = SVideoModel(allItems: children, currentSnapshot: se)
let player = SVideoPlayer(completionHandler: {

3
kplayer/master/KSettingsView.swift

@ -24,6 +24,9 @@ struct KSettingsView: View {
Toggle(isOn: $kSettings.edit, label: {
Text("Edit")
})
Toggle(isOn: $kSettings.newPlayer, label: {
Text("New Player")
})
}
}
Button(action: {

103
kplayer/svideo/SVideoPlayer.swift

@ -7,14 +7,28 @@ import Foundation
import SwiftUI
import AVKit
struct SVideoPlayer : View {
struct SVideoPlayer: View {
// url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!
var player = AVQueuePlayer(items: [AVPlayerItem]())
var completionHandler: (() -> Void)?
var model: SVideoModel
// The progress through the video, as a percentage (from 0 to 1)
@State private var videoPos: Double = 0
// The duration of the video in seconds
@State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
@State private var seeking = false
@State private var scale: CGFloat = 1.0
@State private var lastScaleValue: CGFloat = 1.0
@State private var lastDragOffset: CGSize = CGSize.zero
@State private var dragOffset: CGSize = CGSize.zero
var body: some View {
VStack {
HStack {
Button(action: {
completionHandler!()
@ -22,36 +36,95 @@ struct SVideoPlayer : View {
Text("cancel")
}).buttonStyle(BorderlessButtonStyle());
ScrollView (.horizontal, showsIndicators: false) {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(model.allItems) { item in
Button(action: {
gotoSnapshot(item)
}) {
if item.image != nil {
Image(uiImage: item.image!)
AsyncImage(item: item, placeholder: { Text("Loading ...") },
image: { Image(uiImage: $0).resizable() })
}
}
else {
Image("Kirschkeks-256x256.png")
}
}.frame(height: 50)
Spacer()
}
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.scaleEffect(scale)
.offset(dragOffset)
.gesture(
DragGesture()
.onChanged { gesture in
let dragged = gesture.translation
if move(dragged) {
// lastDragOffset = gesture.translation
dragOffset = CGSize(width: dragged.width + lastDragOffset.width, height: dragged.height + lastDragOffset.height)
}
}
.onEnded { gesture in
lastDragOffset = dragOffset
}
}.frame(height: 100)
)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
Spacer()
//... anything else e.g. clamping the newScale
}.onEnded { val in
// without this the next gesture will be broken
self.lastScaleValue = 1.0
}).clipped()
VideoPlayerControlsView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
}
VideoPlayer(player: player)
.onAppear() {
player.removeAllItems()
player.insert(model.currentPlayerItem(), after: nil)
let item = model.currentPlayerItem()
self.player.replaceCurrentItem(with: item)
// player.removeAllItems()
// player.insert(model.currentPlayerItem(), after: nil)
// Start the player going, otherwise controls don't appear
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player.pause()
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
private func move(_ dragged: CGSize) -> Bool {
// if player.status == .playing {
return true
//}
}
func gotoSnapshot(_ currentSnapshot: MediaItem) {
print(currentSnapshot.time)
player.seek(to: CMTime(seconds: currentSnapshot.time, preferredTimescale: CMTimeScale(10000)))
// player.forceSeekSmoothlyToTime(newChaseTime: currentSnapshot.time)
// loopStart = currentSnapshot.time
// player.loopEnd = loopStart + currentSnapshot.length
//
// if loopMode && currentSnapshot.scale > 0 {
// player.zoom = Float(currentSnapshot.scale)
// player.xpos = currentSnapshot.offset.x
// player.ypos = currentSnapshot.offset.y
// player.transformLayer()
// }
model.currentSnapshot = currentSnapshot
}
}

170
kplayer/svideo/VideoPlayerView.swift

@ -0,0 +1,170 @@
//
// Created by Marco Schmickler on 28.11.21.
// Copyright (c) 2021 Marco Schmickler. All rights reserved.
//
import Foundation
import SwiftUI
import AVFoundation
// This is the UIView that contains the AVPlayerLayer for rendering the video
class VideoPlayerUIView: UIView {
private let player: AVPlayer
private let playerLayer = AVPlayerLayer()
private let videoPos: Binding<Double>
private let videoDuration: Binding<Double>
private let seeking: Binding<Bool>
private var durationObservation: NSKeyValueObservation?
private var timeObservation: Any?
init(player: AVPlayer, videoPos: Binding<Double>, videoDuration: Binding<Double>, seeking: Binding<Bool>) {
self.player = player
self.videoDuration = videoDuration
self.videoPos = videoPos
self.seeking = seeking
super.init(frame: .zero)
backgroundColor = .black
playerLayer.player = player
layer.addSublayer(playerLayer)
// Observe the duration of the player's item so we can display it
// and use it for updating the seek bar's position
durationObservation = player.currentItem?.observe(\.duration, changeHandler: { [weak self] item, change in
guard let self = self else { return }
self.videoDuration.wrappedValue = item.duration.seconds
})
// Observe the player's time periodically so we can update the seek bar's
// position as we progress through playback
timeObservation = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: nil) { [weak self] time in
guard let self = self else { return }
// If we're not seeking currently (don't want to override the slider
// position if the user is interacting)
guard !self.seeking.wrappedValue else {
return
}
self.videoDuration.wrappedValue = self.player.currentItem!.duration.seconds
// update videoPos with the new video time (as a percentage)
self.videoPos.wrappedValue = time.seconds / self.videoDuration.wrappedValue
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func cleanUp() {
// Remove observers we setup in init
durationObservation?.invalidate()
durationObservation = nil
if let observation = timeObservation {
player.removeTimeObserver(observation)
timeObservation = nil
}
}
}
// This is the SwiftUI view which wraps the UIKit-based PlayerUIView above
struct VideoPlayerView: UIViewRepresentable {
@Binding private(set) var videoPos: Double
@Binding private(set) var videoDuration: Double
@Binding private(set) var seeking: Bool
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
// This function gets called if the bindings change, which could be useful if
// you need to respond to external changes, but we don't in this example
}
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
let uiView = VideoPlayerUIView(player: player,
videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking)
return uiView
}
static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
guard let playerUIView = uiView as? VideoPlayerUIView else {
return
}
playerUIView.cleanUp()
}
}
// This is the SwiftUI view that contains the controls for the player
struct VideoPlayerControlsView : View {
@Binding private(set) var videoPos: Double
@Binding private(set) var videoDuration: Double
@Binding private(set) var seeking: Bool
let player: AVPlayer
@State var playerPaused = true
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "play" : "pause")
.padding(.trailing, 10)
}
// Current video time
Text("\(Utility.formatSecondsToHMS(videoPos * videoDuration))")
// Slider for seeking / showing video progress
Slider(value: $videoPos, in: 0...1, onEditingChanged: sliderEditingChanged)
// Video duration
Text("\(Utility.formatSecondsToHMS(videoDuration))")
}
.padding(.leading, 10)
.padding(.trailing, 10)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}

51
kplayer/util/AsyncImage.swift

@ -0,0 +1,51 @@
import SwiftUI
import Haneke
struct AsyncImage<Placeholder: View>: View {
@StateObject private var item: MediaItem
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
item: MediaItem,
@ViewBuilder placeholder: () -> Placeholder,
@ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_item = StateObject(wrappedValue: item)
setImage(newItem: item)
}
var body: some View {
content
// .onAppear(perform: loader.load)
}
private var content: some View {
Group {
if item.image != nil {
image(item.image!)
} else {
placeholder
}
}
}
func setImage(newItem: MediaItem) {
if newItem.image != nil {
let icon = newItem.image!.scaleToSize(66.0, height: 44.0)
} else {
if newItem.thumbUrl != nil {
let URL = Foundation.URL(string: newItem.thumbUrlAbsolute)!
Shared.imageCache.fetch(URL: URL).onSuccess {
i in
newItem.image = i.scaleToSize(66.0, height: 44.0)
}
}
}
}
}

30
kplayer/util/Utility.swift

@ -0,0 +1,30 @@
//
// Utility.swift
// AVPlayer-SwiftUI
//
// Created by Chris Mash on 11/09/2019.
// Copyright © 2019 Chris Mash. All rights reserved.
//
import Foundation
class Utility: NSObject {
private static var timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = [.pad]
return formatter
}()
static func formatSecondsToHMS(_ seconds: Double) -> String {
guard !seconds.isNaN,
let text = timeHMSFormatter.string(from: seconds) else {
return "00:00"
}
return text
}
}
Loading…
Cancel
Save