mirror of
git://jb55.com/damus
synced 2024-10-06 11:43:21 +00:00
Add Full-Bleed Video Player
Changelog-Added: Add new full-bleed video player
This commit is contained in:
commit
a6fb175b98
@ -49,6 +49,7 @@
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
|
||||
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; };
|
||||
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; };
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */; };
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
@ -207,6 +208,8 @@
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
|
||||
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */; };
|
||||
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */; };
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
@ -459,6 +462,7 @@
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoPlayer.swift; sourceTree = "<group>"; };
|
||||
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||
@ -651,6 +655,7 @@
|
||||
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
|
||||
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
|
||||
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
|
||||
@ -771,6 +776,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */,
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -949,6 +955,15 @@
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C1A9A2829DDF53B00516EAC /* Video */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
|
||||
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */,
|
||||
);
|
||||
path = Video;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -975,6 +990,7 @@
|
||||
4C7D09692A0AEA0400943473 /* CodeScanner */,
|
||||
4C7D095A2A098C5C00943473 /* Wallet */,
|
||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
|
||||
4C1A9A2829DDF53B00516EAC /* Video */,
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
@ -1501,6 +1517,7 @@
|
||||
packageProductDependencies = (
|
||||
4C649880286E0EE300EAE2B3 /* secp256k1 */,
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */,
|
||||
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@ -1605,6 +1622,7 @@
|
||||
packageReferences = (
|
||||
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -1669,6 +1687,7 @@
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */,
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||
@ -1814,6 +1833,7 @@
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
@ -2441,6 +2461,14 @@
|
||||
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
|
||||
};
|
||||
};
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/wxxsw/GSPlayer";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.2.26;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@ -2454,6 +2482,11 @@
|
||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||
productName = secp256k1;
|
||||
};
|
||||
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */;
|
||||
productName = GSPlayer;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
@ -1,5 +1,14 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "gsplayer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wxxsw/GSPlayer",
|
||||
"state" : {
|
||||
"revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8",
|
||||
"version" : "0.2.26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -54,7 +54,7 @@ enum ImageShape {
|
||||
|
||||
// MARK: - Image Carousel
|
||||
struct ImageCarousel: View {
|
||||
var urls: [URL]
|
||||
var urls: [MediaUrl]
|
||||
|
||||
let evid: String
|
||||
|
||||
@ -69,11 +69,13 @@ struct ImageCarousel: View {
|
||||
@State private var firstImageHeight: CGFloat? = nil
|
||||
@State private var currentImageHeight: CGFloat?
|
||||
@State private var selectedIndex = 0
|
||||
@State private var video_size: CGSize? = nil
|
||||
|
||||
init(state: DamusState, evid: String, urls: [URL]) {
|
||||
init(state: DamusState, evid: String, urls: [MediaUrl]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
_image_fill = State(initialValue: media_model.fill)
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
@ -102,77 +104,108 @@ struct ImageCarousel: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.image_fill == nil,
|
||||
let meta = state.events.lookup_img_metadata(url: url),
|
||||
let size = meta.meta.dim?.size
|
||||
{
|
||||
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Images: some View {
|
||||
func video_model(_ url: URL) -> VideoPlayerModel {
|
||||
return state.events.get_video_player_model(url: url)
|
||||
}
|
||||
|
||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||
Group {
|
||||
switch url {
|
||||
case .image(let url):
|
||||
Img(geo: geo, url: url, index: index)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
|
||||
.onChange(of: video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
|
||||
print("video_size changed \(size)")
|
||||
if self.image_fill == nil {
|
||||
print("video_size firstImageHeight \(fill.height)")
|
||||
firstImageHeight = fill.height
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
}
|
||||
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Img(geo: GeometryProxy, url: URL, index: Int) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
if index == 0 {
|
||||
firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.padding(0)
|
||||
|
||||
}
|
||||
|
||||
var Medias: some View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
let url = urls[index]
|
||||
GeometryReader { geo in
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
if index == 0 {
|
||||
firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.padding(0)
|
||||
Media(geo: geo, url: urls[index], index: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
|
||||
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
|
||||
}
|
||||
.frame(height: height)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
.onChange(of: selectedIndex) { value in
|
||||
selectedIndex = value
|
||||
}
|
||||
selectedIndex = value
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Images
|
||||
Medias
|
||||
.onTapGesture { }
|
||||
|
||||
// This is our custom carousel image indicator
|
||||
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
|
||||
@ -181,8 +214,8 @@ struct ImageCarousel: View {
|
||||
}
|
||||
|
||||
// MARK: - Custom Carousel
|
||||
struct CarouselDotsView: View {
|
||||
let urls: [URL]
|
||||
struct CarouselDotsView<T>: View {
|
||||
let urls: [T]
|
||||
@Binding var selectedIndex: Int
|
||||
|
||||
var body: some View {
|
||||
@ -254,7 +287,8 @@ public struct ImageFill {
|
||||
// MARK: - Preview Provider
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,10 @@ class RelativeTimeModel: ObservableObject {
|
||||
@Published var value: String = ""
|
||||
}
|
||||
|
||||
class MediaMetaModel: ObservableObject {
|
||||
@Published var fill: ImageFill? = nil
|
||||
}
|
||||
|
||||
class EventData {
|
||||
var translations_model: TranslationModel
|
||||
var artifacts_model: NoteArtifactsModel
|
||||
@ -108,6 +112,7 @@ class EventData {
|
||||
var zaps_model : ZapsDataModel
|
||||
var relative_time: RelativeTimeModel = RelativeTimeModel()
|
||||
var validated: ValidationResult
|
||||
var media_metadata_model: MediaMetaModel
|
||||
|
||||
var translations: TranslateStatus {
|
||||
return translations_model.state
|
||||
@ -126,6 +131,7 @@ class EventData {
|
||||
self.artifacts_model = .init(state: .not_loaded)
|
||||
self.zaps_model = .init(zaps)
|
||||
self.validated = .unknown
|
||||
self.media_metadata_model = MediaMetaModel()
|
||||
self.preview_model = .init(state: .not_loaded)
|
||||
}
|
||||
}
|
||||
@ -135,6 +141,7 @@ class EventCache {
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var image_metadata: [String: ImageMetadataState] = [:]
|
||||
private var video_meta: [String: VideoPlayerModel] = [:]
|
||||
private var event_data: [String: EventData] = [:]
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
@ -194,6 +201,28 @@ class EventCache {
|
||||
return image_metadata[url.absoluteString.lowercased()]
|
||||
}
|
||||
|
||||
func lookup_media_size(url: URL) -> CGSize? {
|
||||
if let img_meta = lookup_img_metadata(url: url) {
|
||||
return img_meta.meta.dim?.size
|
||||
}
|
||||
|
||||
return get_video_player_model(url: url).size
|
||||
}
|
||||
|
||||
func store_video_player_model(url: URL, meta: VideoPlayerModel) {
|
||||
video_meta[url.absoluteString] = meta
|
||||
}
|
||||
|
||||
func get_video_player_model(url: URL) -> VideoPlayerModel {
|
||||
if let model = video_meta[url.absoluteString] {
|
||||
return model
|
||||
}
|
||||
|
||||
let model = VideoPlayerModel()
|
||||
video_meta[url.absoluteString] = model
|
||||
return model
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
|
||||
@ -257,6 +286,7 @@ class EventCache {
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
video_meta = [:]
|
||||
event_data = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
|
@ -66,22 +66,12 @@ enum PreviewState {
|
||||
|
||||
class PreviewCache {
|
||||
private var previews: [String: Preview]
|
||||
private var image_meta: [String: ImageFill]
|
||||
|
||||
func lookup(_ evid: String) -> Preview? {
|
||||
return previews[evid]
|
||||
}
|
||||
|
||||
func lookup_image_meta(_ evid: String) -> ImageFill? {
|
||||
return image_meta[evid]
|
||||
}
|
||||
|
||||
func cache_image_meta(evid: String, image_fill: ImageFill) {
|
||||
self.image_meta[evid] = image_fill
|
||||
}
|
||||
|
||||
init() {
|
||||
self.previews = [:]
|
||||
self.image_meta = [:]
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ import Kingfisher
|
||||
|
||||
|
||||
struct ImageContainerView: View {
|
||||
let url: URL?
|
||||
let cache: EventCache
|
||||
let url: MediaUrl
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
@ -26,8 +27,7 @@ struct ImageContainerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
func Img(url: URL) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note, disable_animation: disable_animation)
|
||||
.configure { view in
|
||||
@ -40,12 +40,23 @@ struct ImageContainerView: View {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch url {
|
||||
case .image(let url):
|
||||
Img(url: url)
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: cache.get_video_player_model(url: url), video_size: .constant(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
|
||||
|
||||
struct ImageContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageContainerView(url: test_image_url, disable_animation: false)
|
||||
ImageContainerView(cache: test_damus_state().events, url: .image(test_image_url), disable_animation: false)
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
|
||||
let urls: [URL?]
|
||||
let cache: EventCache
|
||||
let urls: [MediaUrl]
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@ -39,7 +39,7 @@ struct ImageView: View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(url: urls[index], disable_animation: disable_animation)
|
||||
ImageContainerView(cache: cache, url: urls[index], disable_animation: disable_animation)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
@ -79,6 +79,7 @@ struct ImageView: View {
|
||||
|
||||
struct ImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")], disable_animation: false)
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageView(cache: test_damus_state().events, urls: [url], disable_animation: false)
|
||||
}
|
||||
}
|
||||
|
@ -130,11 +130,11 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if show_images && artifacts.images.count > 0 {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
|
||||
} else if !show_images && artifacts.images.count > 0 {
|
||||
if show_images && artifacts.media.count > 0 {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
} else if !show_images && artifacts.media.count > 0 {
|
||||
ZStack {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
Blur()
|
||||
.disabled(true)
|
||||
}
|
||||
@ -261,13 +261,24 @@ struct NoteArtifacts: Equatable {
|
||||
}
|
||||
|
||||
let content: CompatibleText
|
||||
let images: [URL]
|
||||
let urls: [UrlType]
|
||||
let invoices: [Invoice]
|
||||
let links: [URL]
|
||||
|
||||
var media: [MediaUrl] {
|
||||
return urls.compactMap { url in url.is_media }
|
||||
}
|
||||
|
||||
var images: [URL] {
|
||||
return urls.compactMap { url in url.is_img }
|
||||
}
|
||||
|
||||
var links: [URL] {
|
||||
return urls.compactMap { url in url.is_link }
|
||||
}
|
||||
|
||||
static func just_content(_ content: String) -> NoteArtifacts {
|
||||
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
|
||||
return NoteArtifacts(content: txt, images: [], invoices: [], links: [])
|
||||
return NoteArtifacts(content: txt, urls: [], invoices: [])
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,8 +315,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
||||
|
||||
func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
var invoices: [Invoice] = []
|
||||
var img_urls: [URL] = []
|
||||
var link_urls: [URL] = []
|
||||
var urls: [UrlType] = []
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({ $0.is_note_mention })
|
||||
@ -323,12 +333,14 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
var trimmed = txt
|
||||
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) {
|
||||
if let prev = blocks[safe: ind-1],
|
||||
case .url(let u) = prev,
|
||||
classify_url(u).is_media != nil {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
}
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, is_image_url(u) {
|
||||
if case .url(let u) = next, classify_url(u).is_media != nil {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next, m.type == .event, one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
@ -345,25 +357,112 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
invoices.append(invoice)
|
||||
return str
|
||||
case .url(let url):
|
||||
// Handle Image URLs
|
||||
if is_image_url(url) {
|
||||
// Append Image
|
||||
img_urls.append(url)
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
return str
|
||||
} else {
|
||||
link_urls.append(url)
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
return str + url_str(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
||||
return NoteArtifacts(content: txt, urls: urls, invoices: invoices)
|
||||
}
|
||||
|
||||
func is_image_url(_ url: URL) -> Bool {
|
||||
enum MediaUrl {
|
||||
case image(URL)
|
||||
case video(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UrlType {
|
||||
case media(MediaUrl)
|
||||
case link(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_video: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image:
|
||||
return nil
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_img: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video:
|
||||
return url
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_link: URL? {
|
||||
switch self {
|
||||
case .media:
|
||||
return nil
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_media: MediaUrl? {
|
||||
switch self {
|
||||
case .media(let murl):
|
||||
return murl
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func classify_url(_ url: URL) -> UrlType {
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
let isUrl = str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp")
|
||||
return isUrl
|
||||
|
||||
if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") {
|
||||
return .media(.image(url))
|
||||
}
|
||||
|
||||
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") {
|
||||
return .media(.video(url))
|
||||
}
|
||||
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? {
|
||||
|
77
damus/Views/Video/DamusVideoPlayer.swift
Normal file
77
damus/Views/Video/DamusVideoPlayer.swift
Normal file
@ -0,0 +1,77 @@
|
||||
//
|
||||
// VideoPlayerView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-05.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DamusVideoPlayer: View {
|
||||
var url: URL
|
||||
@ObservedObject var model: VideoPlayerModel
|
||||
@Binding var video_size: CGSize?
|
||||
|
||||
var mute_icon: String {
|
||||
if model.has_audio == false || model.muted {
|
||||
return "speaker.slash"
|
||||
} else {
|
||||
return "speaker"
|
||||
}
|
||||
}
|
||||
|
||||
var mute_icon_color: Color {
|
||||
switch self.model.has_audio {
|
||||
case .none:
|
||||
return .white
|
||||
case .some(let has_audio):
|
||||
return has_audio ? .white : .red
|
||||
}
|
||||
}
|
||||
|
||||
var MuteIcon: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.opacity(0.2)
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.black)
|
||||
|
||||
Image(systemName: mute_icon)
|
||||
.padding()
|
||||
.foregroundColor(mute_icon_color)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
VideoPlayer(url: url, model: model)
|
||||
.onAppear{
|
||||
model.start()
|
||||
}
|
||||
.onDisappear {
|
||||
model.stop()
|
||||
}
|
||||
|
||||
if model.has_audio == true {
|
||||
MuteIcon
|
||||
.zIndex(11.0)
|
||||
.onTapGesture {
|
||||
self.model.muted = !self.model.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: model.size) { size in
|
||||
guard let size else {
|
||||
return
|
||||
}
|
||||
video_size = size
|
||||
}
|
||||
}
|
||||
}
|
||||
struct DamusVideoPlayer_Previews: PreviewProvider {
|
||||
@StateObject static var model: VideoPlayerModel = VideoPlayerModel()
|
||||
|
||||
static var previews: some View {
|
||||
DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, model: model, video_size: .constant(nil))
|
||||
}
|
||||
}
|
347
damus/Views/Video/VideoPlayer.swift
Normal file
347
damus/Views/Video/VideoPlayer.swift
Normal file
@ -0,0 +1,347 @@
|
||||
//
|
||||
// VideoPlayer.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-05-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//
|
||||
// VideoPlayer.swift
|
||||
// VideoPlayer
|
||||
//
|
||||
// Created by Gesen on 2019/7/7.
|
||||
// Copyright © 2019 Gesen. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import GSPlayer
|
||||
import SwiftUI
|
||||
|
||||
public enum VideoState {
|
||||
/// From the first load to get the first frame of the video
|
||||
case loading
|
||||
|
||||
/// Playing now
|
||||
case playing(totalDuration: Double)
|
||||
|
||||
/// Pause, will be called repeatedly when the buffer progress changes
|
||||
case paused(playProgress: Double, bufferProgress: Double)
|
||||
|
||||
/// An error occurred and cannot continue playing
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
enum VideoHandler {
|
||||
case onBufferChanged((Double) -> Void)
|
||||
case onPlayToEndTime(() -> Void)
|
||||
case onReplay(() -> Void)
|
||||
case onStateChanged((VideoState) -> Void)
|
||||
}
|
||||
|
||||
public class VideoPlayerModel: ObservableObject {
|
||||
@Published var autoReplay: Bool = true
|
||||
@Published var muted: Bool = true
|
||||
@Published var play: Bool = true
|
||||
@Published var size: CGSize? = nil
|
||||
@Published var has_audio: Bool? = nil
|
||||
@Published var contentMode: UIView.ContentMode = .scaleAspectFill
|
||||
|
||||
var time: CMTime = CMTime()
|
||||
var handlers: [VideoHandler] = []
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.play = false
|
||||
}
|
||||
|
||||
func start() {
|
||||
self.play = true
|
||||
}
|
||||
|
||||
func mute() {
|
||||
self.muted = true
|
||||
}
|
||||
|
||||
func unmute() {
|
||||
self.muted = false
|
||||
}
|
||||
|
||||
/// Whether the video will be automatically replayed until the end of the video playback.
|
||||
func autoReplay(_ value: Bool) -> Self {
|
||||
autoReplay = value
|
||||
return self
|
||||
}
|
||||
|
||||
/// Whether the video is muted, only for this instance.
|
||||
func mute(_ value: Bool) -> Self {
|
||||
muted = value
|
||||
return self
|
||||
}
|
||||
|
||||
/// A string defining how the video is displayed within an AVPlayerLayer bounds rect.
|
||||
/// scaleAspectFill -> resizeAspectFill, scaleAspectFit -> resizeAspect, other -> resize
|
||||
func contentMode(_ value: UIView.ContentMode) -> Self {
|
||||
contentMode = value
|
||||
return self
|
||||
}
|
||||
|
||||
/// Trigger a callback when the buffer progress changes,
|
||||
/// the value is between 0 and 1.
|
||||
func onBufferChanged(_ handler: @escaping (Double) -> Void) -> Self {
|
||||
self.handlers.append(.onBufferChanged(handler))
|
||||
return self
|
||||
}
|
||||
|
||||
/// Playing to the end.
|
||||
func onPlayToEndTime(_ handler: @escaping () -> Void) -> Self {
|
||||
self.handlers.append(.onPlayToEndTime(handler))
|
||||
return self
|
||||
}
|
||||
|
||||
/// Replay after playing to the end.
|
||||
func onReplay(_ handler: @escaping () -> Void) -> Self {
|
||||
self.handlers.append(.onReplay(handler))
|
||||
return self
|
||||
}
|
||||
|
||||
/// Playback status changes, such as from play to pause.
|
||||
func onStateChanged(_ handler: @escaping (VideoState) -> Void) -> Self {
|
||||
self.handlers.append(.onStateChanged(handler))
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
public struct VideoPlayer {
|
||||
private(set) var url: URL
|
||||
|
||||
@ObservedObject var model: VideoPlayerModel
|
||||
|
||||
/// Init video player instance.
|
||||
/// - Parameters:
|
||||
/// - url: http/https URL
|
||||
/// - play: play/pause
|
||||
/// - time: current time
|
||||
public init(url: URL, model: VideoPlayerModel) {
|
||||
self.url = url
|
||||
self._model = ObservedObject(wrappedValue: model)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
public extension VideoPlayer {
|
||||
|
||||
/// Set the preload size, the default value is 1024 * 1024, unit is byte.
|
||||
static var preloadByteCount: Int {
|
||||
get { VideoPreloadManager.shared.preloadByteCount }
|
||||
set { VideoPreloadManager.shared.preloadByteCount = newValue }
|
||||
}
|
||||
|
||||
/// Set the video urls to be preload queue.
|
||||
/// Preloading will automatically cache a short segment of the beginning of the video
|
||||
/// and decide whether to start or pause the preload based on the buffering of the currently playing video.
|
||||
/// - Parameter urls: URL array
|
||||
static func preload(urls: [URL]) {
|
||||
VideoPreloadManager.shared.set(waiting: urls)
|
||||
}
|
||||
|
||||
/// Set custom http header, such as token.
|
||||
static func customHTTPHeaderFields(transform: @escaping (URL) -> [String: String]?) {
|
||||
VideoLoadManager.shared.customHTTPHeaderFields = transform
|
||||
}
|
||||
|
||||
/// Get the total size of the video cache.
|
||||
static func calculateCachedSize() -> UInt {
|
||||
return VideoCacheManager.calculateCachedSize()
|
||||
}
|
||||
|
||||
/// Clean up all caches.
|
||||
static func cleanAllCache() {
|
||||
try? VideoCacheManager.cleanAllCache()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
public extension VideoPlayer {
|
||||
|
||||
|
||||
}
|
||||
|
||||
func get_video_size(player: AVPlayer) -> CGSize? {
|
||||
// TODO: make this async?
|
||||
return player.currentImage?.size
|
||||
}
|
||||
|
||||
func video_has_audio(player: AVPlayer) async -> Bool {
|
||||
let tracks = try? await player.currentItem?.asset.load(.tracks)
|
||||
return tracks?.filter({ t in t.mediaType == .audio }).first != nil
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension VideoPlayer: UIViewRepresentable {
|
||||
|
||||
public func makeUIView(context: Context) -> VideoPlayerView {
|
||||
let uiView = VideoPlayerView()
|
||||
|
||||
uiView.playToEndTime = {
|
||||
if self.model.autoReplay == false {
|
||||
self.model.play = false
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
for handler in model.handlers {
|
||||
if case .onPlayToEndTime(let cb) = handler {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiView.contentMode = self.model.contentMode
|
||||
|
||||
uiView.replay = {
|
||||
DispatchQueue.main.async {
|
||||
for handler in model.handlers {
|
||||
if case .onReplay(let cb) = handler {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiView.stateDidChanged = { [unowned uiView] _ in
|
||||
let state: VideoState = uiView.convertState()
|
||||
|
||||
if case .playing = state {
|
||||
context.coordinator.startObserver(uiView: uiView)
|
||||
|
||||
if let player = uiView.player {
|
||||
Task {
|
||||
let has_audio = await video_has_audio(player: player)
|
||||
let size = get_video_size(player: player)
|
||||
Task { @MainActor in
|
||||
if let size {
|
||||
self.model.size = size
|
||||
}
|
||||
self.model.has_audio = has_audio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
context.coordinator.stopObserver(uiView: uiView)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for handler in model.handlers {
|
||||
if case .onStateChanged(let cb) = handler {
|
||||
cb(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uiView
|
||||
}
|
||||
|
||||
public func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
public func updateUIView(_ uiView: VideoPlayerView, context: Context) {
|
||||
if context.coordinator.observingURL != url {
|
||||
context.coordinator.clean()
|
||||
context.coordinator.observingURL = url
|
||||
}
|
||||
|
||||
if model.play {
|
||||
uiView.play(for: url)
|
||||
} else {
|
||||
uiView.pause(reason: .userInteraction)
|
||||
}
|
||||
|
||||
uiView.isMuted = model.muted
|
||||
uiView.isAutoReplay = model.autoReplay
|
||||
|
||||
if let observerTime = context.coordinator.observerTime, model.time != observerTime {
|
||||
uiView.seek(to: model.time, toleranceBefore: model.time, toleranceAfter: model.time, completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
public static func dismantleUIView(_ uiView: VideoPlayerView, coordinator: VideoPlayer.Coordinator) {
|
||||
uiView.pause(reason: .hidden)
|
||||
}
|
||||
|
||||
public class Coordinator: NSObject {
|
||||
var videoPlayer: VideoPlayer
|
||||
var observingURL: URL?
|
||||
var observer: Any?
|
||||
var observerTime: CMTime?
|
||||
var observerBuffer: Double?
|
||||
|
||||
init(_ videoPlayer: VideoPlayer) {
|
||||
self.videoPlayer = videoPlayer
|
||||
}
|
||||
|
||||
func startObserver(uiView: VideoPlayerView) {
|
||||
guard observer == nil else { return }
|
||||
|
||||
observer = uiView.addPeriodicTimeObserver(forInterval: .init(seconds: 0.25, preferredTimescale: 60)) { [weak self, unowned uiView] time in
|
||||
guard let `self` = self else { return }
|
||||
|
||||
self.videoPlayer.model.time = time
|
||||
self.observerTime = time
|
||||
|
||||
self.updateBuffer(uiView: uiView)
|
||||
}
|
||||
}
|
||||
|
||||
func stopObserver(uiView: VideoPlayerView) {
|
||||
guard let observer = observer else { return }
|
||||
|
||||
uiView.removeTimeObserver(observer)
|
||||
|
||||
self.observer = nil
|
||||
}
|
||||
|
||||
func clean() {
|
||||
self.observingURL = nil
|
||||
self.observer = nil
|
||||
self.observerTime = nil
|
||||
self.observerBuffer = nil
|
||||
}
|
||||
|
||||
func updateBuffer(uiView: VideoPlayerView) {
|
||||
let bufferProgress = uiView.bufferProgress
|
||||
guard bufferProgress != observerBuffer else { return }
|
||||
|
||||
for handler in videoPlayer.model.handlers {
|
||||
if case .onBufferChanged(let cb) = handler {
|
||||
DispatchQueue.main.async {
|
||||
cb(bufferProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observerBuffer = bufferProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VideoPlayerView {
|
||||
|
||||
func convertState() -> VideoState {
|
||||
switch state {
|
||||
case .none, .loading:
|
||||
return .loading
|
||||
case .playing:
|
||||
return .playing(totalDuration: totalDuration)
|
||||
case .paused(let p, let b):
|
||||
return .paused(playProgress: p, bufferProgress: b)
|
||||
case .error(let error):
|
||||
return .error(error)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user