Compare commits
7 Commits
a4cd4d6adb
...
5c782e09f5
Author | SHA1 | Date |
---|---|---|
yicheng | 5c782e09f5 | |
yicheng | 003a81d703 | |
dependabot[bot] | 31de56716d | |
yicheng | 68c3aff948 | |
yicheng | 93a4c03ca7 | |
yicheng | 118bb3dff3 | |
Yam Liu | 09d30ee16c |
|
@ -7,7 +7,7 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: Build ipa
|
||||
runs-on: macos-13
|
||||
runs-on: macos-14
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -23,21 +23,21 @@ jobs:
|
|||
bundle install -j 4
|
||||
bundle exec fastlane build_unsign_ipa
|
||||
|
||||
- name: List, filter and delete artifacts
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
uses: actions/github-script@v6
|
||||
id: artifact
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.issue
|
||||
# - name: List, filter and delete artifacts
|
||||
# if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
# uses: actions/github-script@v6
|
||||
# id: artifact
|
||||
# with:
|
||||
# script: |
|
||||
# const { owner, repo } = context.issue
|
||||
|
||||
const res = await github.rest.actions.listArtifactsForRepo({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
# const res = await github.rest.actions.listArtifactsForRepo({
|
||||
# owner,
|
||||
# repo,
|
||||
# })
|
||||
|
||||
res.data.artifacts
|
||||
.forEach(({ id }) => { github.rest.actions.deleteArtifact({ owner, repo, artifact_id: id, }) })
|
||||
# res.data.artifacts
|
||||
# .forEach(({ id }) => { github.rest.actions.deleteArtifact({ owner, repo, artifact_id: id, }) })
|
||||
|
||||
- name: Upload latest artifact
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
@ -45,4 +45,4 @@ jobs:
|
|||
with:
|
||||
name: main-unsigned.ipa
|
||||
path: BilbiliAtvDemo.ipa
|
||||
retention-days: 30
|
||||
retention-days: 60
|
||||
|
|
|
@ -11,15 +11,19 @@
|
|||
0A41EE1C2A63102B0066444C /* dm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41EE1A2A63102B0066444C /* dm.pb.swift */; };
|
||||
0A41EE1D2A63102B0066444C /* dmView.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41EE1B2A63102B0066444C /* dmView.pb.swift */; };
|
||||
27FECFCC2B0B98F400EC6A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 27FECFCB2B0B98F400EC6A6D /* Localizable.xcstrings */; };
|
||||
2806E51E2C1593A000164C10 /* LookinServer in Frameworks */ = {isa = PBXBuildFile; productRef = 2806E51D2C1593A000164C10 /* LookinServer */; };
|
||||
2806E5202C15A59E00164C10 /* ReplyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */; };
|
||||
2806E5232C16011B00164C10 /* ReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2806E5212C16011B00164C10 /* ReplyCell.swift */; };
|
||||
2806E5242C16011B00164C10 /* ReplyCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2806E5222C16011B00164C10 /* ReplyCell.xib */; };
|
||||
2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBE4C4C2628818F00D20413 /* HistoryViewController.swift */; };
|
||||
490425F729AB54B200CDBC60 /* CategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490425F629AB54B200CDBC60 /* CategoryViewController.swift */; };
|
||||
49078E47291BEA2400F556BD /* PocketSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 49078E46291BEA2400F556BD /* PocketSVG */; };
|
||||
490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */; };
|
||||
490EC3E9290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */; };
|
||||
492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; };
|
||||
492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */; };
|
||||
492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */; };
|
||||
492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; };
|
||||
492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */; };
|
||||
492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */; };
|
||||
492AD70F2BFF6761007221C8 /* NewVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */; };
|
||||
492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */; };
|
||||
492AD7132C001CA7007221C8 /* String+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7122C001CA7007221C8 /* String+Error.swift */; };
|
||||
|
@ -38,15 +42,18 @@
|
|||
49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E0E2943420100D26812 /* CocoaLumberjack */; };
|
||||
49508E112943420100D26812 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E102943420100D26812 /* CocoaLumberjackSwift */; };
|
||||
496400D32943431E0098ACA6 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496400D22943431E0098ACA6 /* Logger.swift */; };
|
||||
496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */; };
|
||||
496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4E2C0194720062951B /* MaskViewPugin.swift */; };
|
||||
496E5A512C0194CD0062951B /* BUpnpPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */; };
|
||||
496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */; };
|
||||
496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A542C01CDBB0062951B /* DebugPlugin.swift */; };
|
||||
496E5A572C01CDCA0062951B /* SpeedChangerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */; };
|
||||
4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502A29161B770045C26B /* WeeklyWatchViewController.swift */; };
|
||||
4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */; };
|
||||
497361082BF1A16600ED213F /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497361072BF1A16600ED213F /* Keys.swift */; };
|
||||
497CF22F2C16EBA4006E1488 /* Published+..swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF22E2C16EBA4006E1488 /* Published+..swift */; };
|
||||
497CF2312C16ED45006E1488 /* MaskProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2302C16ED45006E1488 /* MaskProvider.swift */; };
|
||||
497CF2372C16EDE5006E1488 /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */; };
|
||||
497CF2382C16EDE5006E1488 /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */; };
|
||||
497CF2392C16EDE5006E1488 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */; };
|
||||
498CF2902B63AABE0009793E /* dictionary_hash.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2422B63AABE0009793E /* dictionary_hash.c */; };
|
||||
498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2432B63AABE0009793E /* backward_references_hq.c */; };
|
||||
498CF2922B63AABE0009793E /* histogram.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2462B63AABE0009793E /* histogram.c */; };
|
||||
|
@ -85,7 +92,6 @@
|
|||
49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */; };
|
||||
49D39F28263AD40000F14497 /* WebRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D39F27263AD40000F14497 /* WebRequest.swift */; };
|
||||
49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */; };
|
||||
49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */; };
|
||||
49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */; };
|
||||
49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */; };
|
||||
49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */ = {isa = PBXBuildFile; fileRef = 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */; };
|
||||
|
@ -119,8 +125,6 @@
|
|||
F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */; };
|
||||
F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */; };
|
||||
F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = F927ED9E2610B5C300EAB8E3 /* Kingfisher */; };
|
||||
F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */; };
|
||||
F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */; };
|
||||
F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E926195EC200F8E66A /* UIView+Layout.swift */; };
|
||||
F99D28F72619F5F000F8E66A /* FollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28F62619F5F000F8E66A /* FollowsViewController.swift */; };
|
||||
F9B57354260F5F7400771ED5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9B57353260F5F7400771ED5 /* AppDelegate.swift */; };
|
||||
|
@ -154,14 +158,17 @@
|
|||
0A41EE1A2A63102B0066444C /* dm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dm.pb.swift; sourceTree = "<group>"; };
|
||||
0A41EE1B2A63102B0066444C /* dmView.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dmView.pb.swift; sourceTree = "<group>"; };
|
||||
27FECFCB2B0B98F400EC6A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDetailViewController.swift; sourceTree = "<group>"; };
|
||||
2806E5212C16011B00164C10 /* ReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReplyCell.swift; path = BilibiliLive/Component/View/ReplyCell.swift; sourceTree = SOURCE_ROOT; };
|
||||
2806E5222C16011B00164C10 /* ReplyCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ReplyCell.xib; path = BilibiliLive/Component/View/ReplyCell.xib; sourceTree = SOURCE_ROOT; };
|
||||
2DBE4C4C2628818F00D20413 /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = "<group>"; };
|
||||
490425F629AB54B200CDBC60 /* CategoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryViewController.swift; sourceTree = "<group>"; };
|
||||
490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = "<group>"; };
|
||||
490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLSettingLineCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = "<group>"; };
|
||||
492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCommonPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanmuViewPlugin.swift; sourceTree = "<group>"; };
|
||||
492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoPlayPlugin.swift; sourceTree = "<group>"; };
|
||||
492AD7122C001CA7007221C8 /* String+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Error.swift"; sourceTree = "<group>"; };
|
||||
|
@ -179,15 +186,18 @@
|
|||
49474212290509F6005D6885 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
|
||||
4947423A2906B308005D6885 /* BLTextOnlyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLTextOnlyCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
496400D22943431E0098ACA6 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = "<group>"; };
|
||||
496E5A4E2C0194720062951B /* MaskViewPugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskViewPugin.swift; sourceTree = "<group>"; };
|
||||
496E5A502C0194CD0062951B /* BUpnpPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BUpnpPlugin.swift; sourceTree = "<group>"; };
|
||||
496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = "<group>"; };
|
||||
496E5A542C01CDBB0062951B /* DebugPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPlugin.swift; sourceTree = "<group>"; };
|
||||
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedChangerPlugin.swift; sourceTree = "<group>"; };
|
||||
4973502A29161B770045C26B /* WeeklyWatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyWatchViewController.swift; sourceTree = "<group>"; };
|
||||
4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardVideoCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
497361072BF1A16600ED213F /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
|
||||
497CF22E2C16EBA4006E1488 /* Published+..swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Published+..swift"; sourceTree = "<group>"; };
|
||||
497CF2302C16ED45006E1488 /* MaskProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskProvider.swift; sourceTree = "<group>"; };
|
||||
497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = "<group>"; };
|
||||
497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = "<group>"; };
|
||||
497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = "<group>"; };
|
||||
498CF2352B63AABE0009793E /* NSData+BrotliCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+BrotliCompression.h"; sourceTree = "<group>"; };
|
||||
498CF2362B63AABE0009793E /* LMBrotliCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LMBrotliCompression.h; sourceTree = "<group>"; };
|
||||
498CF2372B63AABE0009793E /* BrotliKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrotliKit.h; sourceTree = "<group>"; };
|
||||
|
@ -281,7 +291,6 @@
|
|||
49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerMetaUtils.swift; sourceTree = "<group>"; };
|
||||
49D39F27263AD40000F14497 /* WebRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRequest.swift; sourceTree = "<group>"; };
|
||||
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerPlugin.swift; sourceTree = "<group>"; };
|
||||
49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = "<group>"; };
|
||||
49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVInfoPanelCollectionViewThumbnailCell+Hook.swift"; sourceTree = "<group>"; };
|
||||
49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BilibiliVideoResourceLoaderDelegate.swift; sourceTree = "<group>"; };
|
||||
49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = DLNAInfo.xml; sourceTree = "<group>"; };
|
||||
|
@ -312,9 +321,7 @@
|
|||
F927ED8C2610A5A800EAB8E3 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
|
||||
F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
|
||||
F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveViewController.swift; sourceTree = "<group>"; };
|
||||
F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
F99D28DA2618A55900F8E66A /* BilibiliLive-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BilibiliLive-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
F99D28E926195EC200F8E66A /* UIView+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Layout.swift"; sourceTree = "<group>"; };
|
||||
F99D28F62619F5F000F8E66A /* FollowsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsViewController.swift; sourceTree = "<group>"; };
|
||||
F9B57350260F5F7400771ED5 /* BilibiliLive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BilibiliLive.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -349,6 +356,7 @@
|
|||
49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */,
|
||||
499C75EC293058C9003160FB /* CocoaAsyncSocket in Frameworks */,
|
||||
F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */,
|
||||
2806E51E2C1593A000164C10 /* LookinServer in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -387,7 +395,6 @@
|
|||
F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */,
|
||||
F9B57355260F5F7400771ED5 /* LivePlayerViewController.swift */,
|
||||
F927ED672610113A00EAB8E3 /* LiveDanMuProvider.swift */,
|
||||
49D2509F2C118FA700173908 /* URLPlayPlugin.swift */,
|
||||
);
|
||||
path = Live;
|
||||
sourceTree = "<group>";
|
||||
|
@ -395,18 +402,15 @@
|
|||
49389D5D28AFE6DC00B9DAFD /* Video */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */,
|
||||
F9EDADD2262AA421007CB99F /* VideoDetailViewController.swift */,
|
||||
2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */,
|
||||
2806E5212C16011B00164C10 /* ReplyCell.swift */,
|
||||
2806E5222C16011B00164C10 /* ReplyCell.xib */,
|
||||
49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */,
|
||||
498DB1DE291BC24700F95607 /* BMaskProvider.swift */,
|
||||
49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */,
|
||||
492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */,
|
||||
497CF2322C16EDAC006E1488 /* MaskProvider */,
|
||||
492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */,
|
||||
492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */,
|
||||
492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */,
|
||||
496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */,
|
||||
496E5A502C0194CD0062951B /* BUpnpPlugin.swift */,
|
||||
496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */,
|
||||
49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */,
|
||||
497CF2332C16EDC0006E1488 /* Plugins */,
|
||||
);
|
||||
path = Video;
|
||||
sourceTree = "<group>";
|
||||
|
@ -425,6 +429,7 @@
|
|||
AE2B41562914C02000BF2B0B /* Int.swift */,
|
||||
49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */,
|
||||
492AD7122C001CA7007221C8 /* String+Error.swift */,
|
||||
497CF22E2C16EBA4006E1488 /* Published+..swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -440,6 +445,41 @@
|
|||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
497CF2322C16EDAC006E1488 /* MaskProvider */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
498DB1DE291BC24700F95607 /* BMaskProvider.swift */,
|
||||
49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */,
|
||||
497CF2302C16ED45006E1488 /* MaskProvider.swift */,
|
||||
);
|
||||
path = MaskProvider;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
497CF2332C16EDC0006E1488 /* Plugins */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */,
|
||||
497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */,
|
||||
497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */,
|
||||
492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */,
|
||||
496E5A502C0194CD0062951B /* BUpnpPlugin.swift */,
|
||||
);
|
||||
path = Plugins;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
497CF23A2C16EE04006E1488 /* Plugins */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
49D2509F2C118FA700173908 /* URLPlayPlugin.swift */,
|
||||
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */,
|
||||
496E5A4E2C0194720062951B /* MaskViewPugin.swift */,
|
||||
496E5A542C01CDBB0062951B /* DebugPlugin.swift */,
|
||||
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */,
|
||||
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */,
|
||||
);
|
||||
path = Plugins;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
498CF2342B63AABE0009793E /* BrotliKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -665,14 +705,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */,
|
||||
F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */,
|
||||
49FB8EE829208EBE0045D5DE /* SidxParseUtil.swift */,
|
||||
492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */,
|
||||
492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */,
|
||||
496E5A4E2C0194720062951B /* MaskViewPugin.swift */,
|
||||
496E5A542C01CDBB0062951B /* DebugPlugin.swift */,
|
||||
496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */,
|
||||
49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */,
|
||||
492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */,
|
||||
497CF23A2C16EE04006E1488 /* Plugins */,
|
||||
49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */,
|
||||
);
|
||||
path = Player;
|
||||
|
@ -745,6 +780,7 @@
|
|||
49508E0E2943420100D26812 /* CocoaLumberjack */,
|
||||
49508E102943420100D26812 /* CocoaLumberjackSwift */,
|
||||
0A41EE182A630FEA0066444C /* SwiftProtobuf */,
|
||||
2806E51D2C1593A000164C10 /* LookinServer */,
|
||||
);
|
||||
productName = BilibiliLive;
|
||||
productReference = F9B57350260F5F7400771ED5 /* BilibiliLive.app */;
|
||||
|
@ -788,6 +824,7 @@
|
|||
499C76142931A7AE003160FB /* XCRemoteSwiftPackageReference "SwiftyXMLParser" */,
|
||||
49508E0D2943420100D26812 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */,
|
||||
0A41EE172A630FEA0066444C /* XCRemoteSwiftPackageReference "swift-protobuf" */,
|
||||
2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */,
|
||||
);
|
||||
productRefGroup = F9B57351260F5F7400771ED5 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -807,6 +844,7 @@
|
|||
49F918722931E927001D3EC3 /* AvTransportScpd.xml in Resources */,
|
||||
F9B5735E260F5F7600771ED5 /* LaunchScreen.storyboard in Resources */,
|
||||
F9B5735B260F5F7600771ED5 /* Assets.xcassets in Resources */,
|
||||
2806E5242C16011B00164C10 /* ReplyCell.xib in Resources */,
|
||||
49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */,
|
||||
27FECFCC2B0B98F400EC6A6D /* Localizable.xcstrings in Resources */,
|
||||
F9B57359260F5F7400771ED5 /* Main.storyboard in Resources */,
|
||||
|
@ -844,17 +882,18 @@
|
|||
files = (
|
||||
F9B9EAED261B25E40045C2C6 /* ToViewViewController.swift in Sources */,
|
||||
4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */,
|
||||
496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */,
|
||||
AE7A3B20290298BE006FEBB0 /* Colors.swift in Sources */,
|
||||
496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */,
|
||||
49FB8EC0291F4C520045D5DE /* VMaskProvider.swift in Sources */,
|
||||
497361082BF1A16600ED213F /* Keys.swift in Sources */,
|
||||
49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */,
|
||||
498CF2A22B63AABE0009793E /* dictionary.c in Sources */,
|
||||
2806E5202C15A59E00164C10 /* ReplyDetailViewController.swift in Sources */,
|
||||
494741C82902C45D005D6885 /* Array+..swift in Sources */,
|
||||
498CF29E2B63AABE0009793E /* bit_cost.c in Sources */,
|
||||
498CF29B2B63AABE0009793E /* utf8_util.c in Sources */,
|
||||
498CF2AA2B63AABE0009793E /* LMBrotliCompressor.m in Sources */,
|
||||
497CF22F2C16EBA4006E1488 /* Published+..swift in Sources */,
|
||||
AEA6AB1628FFE951007CE72E /* SettingsViewController.swift in Sources */,
|
||||
49389D8928B0A1B700B9DAFD /* UIViewController+Ext.swift in Sources */,
|
||||
AE2B41572914C02000BF2B0B /* Int.swift in Sources */,
|
||||
|
@ -862,10 +901,9 @@
|
|||
498CF2A72B63AABE0009793E /* decode.c in Sources */,
|
||||
F90AAE04265549B5008DE7C2 /* FeedViewController.swift in Sources */,
|
||||
2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */,
|
||||
492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */,
|
||||
492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */,
|
||||
F9171D6629026AC5002868C7 /* TitleSupplementaryView.swift in Sources */,
|
||||
F9B9EAE7261AC6F80045C2C6 /* BLTabBarViewController.swift in Sources */,
|
||||
F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */,
|
||||
498CF2A12B63AABE0009793E /* metablock.c in Sources */,
|
||||
F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */,
|
||||
498CF29D2B63AABE0009793E /* brotli_bit_stream.c in Sources */,
|
||||
|
@ -874,6 +912,7 @@
|
|||
49FB8EE929208EBE0045D5DE /* SidxParseUtil.swift in Sources */,
|
||||
F9171D6129010429002868C7 /* UICollectionView+..swift in Sources */,
|
||||
498CF2982B63AABE0009793E /* encoder_dict.c in Sources */,
|
||||
497CF2312C16ED45006E1488 /* MaskProvider.swift in Sources */,
|
||||
AE2B41552912706700BF2B0B /* SearchResultViewController.swift in Sources */,
|
||||
499C75EE29305A1E003160FB /* BiliBiliUpnpDMR.swift in Sources */,
|
||||
49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */,
|
||||
|
@ -897,7 +936,7 @@
|
|||
492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */,
|
||||
F927ED742610395300EAB8E3 /* DanmakuView.swift in Sources */,
|
||||
F927ED782610395300EAB8E3 /* DanmakuTrack.swift in Sources */,
|
||||
492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */,
|
||||
492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */,
|
||||
498CF29C2B63AABE0009793E /* compress_fragment.c in Sources */,
|
||||
499C760F2930E068003160FB /* NVASocket.swift in Sources */,
|
||||
498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */,
|
||||
|
@ -908,19 +947,20 @@
|
|||
F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */,
|
||||
492AD7132C001CA7007221C8 /* String+Error.swift in Sources */,
|
||||
490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */,
|
||||
2806E5232C16011B00164C10 /* ReplyCell.swift in Sources */,
|
||||
F927ED732610395300EAB8E3 /* DanmakuCell.swift in Sources */,
|
||||
497CF2392C16EDE5006E1488 /* VideoPlayListPlugin.swift in Sources */,
|
||||
498CF2A92B63AABE0009793E /* bit_reader.c in Sources */,
|
||||
498CF2972B63AABE0009793E /* encode.c in Sources */,
|
||||
49389D8C28B0A84500B9DAFD /* PersonalViewController.swift in Sources */,
|
||||
498CF2992B63AABE0009793E /* cluster.c in Sources */,
|
||||
498CF2952B63AABE0009793E /* compress_fragment_two_pass.c in Sources */,
|
||||
49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */,
|
||||
F927ED772610395300EAB8E3 /* DanmakuQueuePool.swift in Sources */,
|
||||
497CF2382C16EDE5006E1488 /* BVideoInfoPlugin.swift in Sources */,
|
||||
49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */,
|
||||
AEA6AB1928FFF3DD007CE72E /* Settings.swift in Sources */,
|
||||
498CF2962B63AABE0009793E /* block_splitter.c in Sources */,
|
||||
494741C6290177BB005D6885 /* UpSpaceViewController.swift in Sources */,
|
||||
496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */,
|
||||
496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */,
|
||||
49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */,
|
||||
492731EE29096677005F5B0A /* HotViewController.swift in Sources */,
|
||||
|
@ -932,7 +972,6 @@
|
|||
49D39F28263AD40000F14497 /* WebRequest.swift in Sources */,
|
||||
498DB1DF291BC24700F95607 /* BMaskProvider.swift in Sources */,
|
||||
494741C029002797005D6885 /* UserDefault+..swift in Sources */,
|
||||
F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */,
|
||||
AE4889B228FE55DA00E8C5CD /* FavoriteViewController.swift in Sources */,
|
||||
F9B57356260F5F7400771ED5 /* LivePlayerViewController.swift in Sources */,
|
||||
498CF29F2B63AABE0009793E /* static_dict.c in Sources */,
|
||||
|
@ -950,6 +989,7 @@
|
|||
F9171D6429010DF1002868C7 /* FeedCollectionViewCell.swift in Sources */,
|
||||
F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */,
|
||||
F927ED792610395400EAB8E3 /* DanmakuCellModel.swift in Sources */,
|
||||
497CF2372C16EDE5006E1488 /* BVideoClipsPlugin.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1190,6 +1230,14 @@
|
|||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/QMUI/LookinServer/";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.2.8;
|
||||
};
|
||||
};
|
||||
49078E45291BEA2400F556BD /* XCRemoteSwiftPackageReference "PocketSVG" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pocketsvg/PocketSVG.git";
|
||||
|
@ -1286,6 +1334,11 @@
|
|||
package = 0A41EE172A630FEA0066444C /* XCRemoteSwiftPackageReference "swift-protobuf" */;
|
||||
productName = SwiftProtobuf;
|
||||
};
|
||||
2806E51D2C1593A000164C10 /* LookinServer */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */;
|
||||
productName = LookinServer;
|
||||
};
|
||||
49078E46291BEA2400F556BD /* PocketSVG */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 49078E45291BEA2400F556BD /* XCRemoteSwiftPackageReference "PocketSVG" */;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"originHash" : "45c5c9267c8c84275fa647f629e1033e605b18efefc17e781b907af506eb5385",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
|
@ -45,6 +46,15 @@
|
|||
"version" : "6.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "lookinserver",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/QMUI/LookinServer/",
|
||||
"state" : {
|
||||
"revision" : "e553d1b689d147817dc54ad5c28fcff71e860101",
|
||||
"version" : "1.2.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "marqueelabel",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -118,5 +128,5 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
"version" : 3
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="21507" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="32700.99.1234" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="appleTV" appearance="dark"/>
|
||||
<dependencies>
|
||||
<deployment identifier="tvOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
|
@ -646,15 +646,15 @@
|
|||
<constraints>
|
||||
<constraint firstAttribute="height" constant="360" id="dCt-K6-G5Q"/>
|
||||
</constraints>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="10" id="q3p-JN-DhJ">
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="q3p-JN-DhJ">
|
||||
<size key="itemSize" width="582" height="360"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="30" maxY="0.0"/>
|
||||
<inset key="sectionInset" minX="60" minY="0.0" maxX="60" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="ReplyCell" id="gxq-A8-Ahf" customClass="ReplyCell" customModule="BilibiliLive" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
|
||||
<rect key="frame" x="60" y="0.0" width="582" height="360"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<collectionViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="91g-YW-NBL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
|
||||
|
@ -801,6 +801,7 @@
|
|||
<outlet property="playButton" destination="lfB-VX-ver" id="9XU-MT-KOf"/>
|
||||
<outlet property="playCountLabel" destination="ahl-Fa-Qpg" id="q6V-be-wNj"/>
|
||||
<outlet property="recommandCollectionView" destination="nw1-cq-9yk" id="w8m-bc-mWQ"/>
|
||||
<outlet property="repliesCollectionViewHeightConstraints" destination="dCt-K6-G5Q" id="ZfP-oq-I5s"/>
|
||||
<outlet property="replysCollectionView" destination="zj0-CY-pLt" id="8h4-LO-bUs"/>
|
||||
<outlet property="scrollView" destination="Dm8-CL-kxU" id="R6I-to-71J"/>
|
||||
<outlet property="titleLabel" destination="kwE-Zo-p5T" id="3Fx-I9-hrS"/>
|
||||
|
@ -936,7 +937,7 @@
|
|||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.0" green="0.0" blue="0.0" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color white="0.0" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -1,266 +1,60 @@
|
|||
//
|
||||
// CommonPlayerViewController.swift
|
||||
// NewCommonPlayerViewController.swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by Etan Chen on 2021/4/4.
|
||||
// Created by yicheng on 2024/5/23.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import Vision
|
||||
|
||||
protocol MaskProvider: AnyObject {
|
||||
func getMask(for time: CMTime, frame: CGRect, onGet: @escaping (CALayer) -> Void)
|
||||
func needVideoOutput() -> Bool
|
||||
func setVideoOutout(ouput: AVPlayerItemVideoOutput)
|
||||
func preferFPS() -> Int
|
||||
}
|
||||
|
||||
class CommonPlayerViewController: AVPlayerViewController {
|
||||
let danMuView = DanmakuView()
|
||||
var allowChangeSpeed = true
|
||||
var playerStartPos: Int?
|
||||
private var retryCount = 0
|
||||
private let maxRetryCount = 3
|
||||
private var observer: NSKeyValueObservation?
|
||||
class CommonPlayerViewController: UIViewController {
|
||||
private let playerVC = AVPlayerViewController()
|
||||
private var activePlugins = [CommonPlayerPlugin]()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
private var rateObserver: NSKeyValueObservation?
|
||||
private var debugView: UILabel?
|
||||
var maskProvider: MaskProvider?
|
||||
|
||||
var playerItem: AVPlayerItem? {
|
||||
didSet {
|
||||
if let playerItem = playerItem {
|
||||
removeObservarPlayerItem()
|
||||
observePlayerItem(playerItem)
|
||||
if let playerInfo = playerInfo {
|
||||
playerItem.externalMetadata = playerInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var player: AVPlayer? {
|
||||
didSet {
|
||||
if let player = player {
|
||||
rateObserver = player.observe(\.rate, options: [.old, .new]) {
|
||||
[weak self] player, _ in
|
||||
guard let self = self else { return }
|
||||
playerRateDidChange(player: player)
|
||||
}
|
||||
danMuView.play()
|
||||
} else {
|
||||
rateObserver = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var videoOutput: AVPlayerItemVideoOutput?
|
||||
|
||||
private var playerInfo: [AVMetadataItem]?
|
||||
|
||||
deinit {
|
||||
stopDebug()
|
||||
}
|
||||
private var statusObserver: NSKeyValueObservation?
|
||||
private var isEnd = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
appliesPreferredDisplayCriteriaAutomatically = Settings.contentMatch
|
||||
allowsPictureInPicturePlayback = true
|
||||
delegate = self
|
||||
initDanmuView()
|
||||
setupPlayerMenu()
|
||||
}
|
||||
addChild(playerVC)
|
||||
view.addSubview(playerVC.view)
|
||||
playerVC.didMove(toParent: self)
|
||||
playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() }
|
||||
playerVC.allowsPictureInPicturePlayback = true
|
||||
playerVC.delegate = self
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
danMuView.recaculateTracks()
|
||||
danMuView.paddingTop = 5
|
||||
danMuView.trackHeight = 50
|
||||
danMuView.displayArea = Settings.danmuArea.percent
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
danMuView.stop()
|
||||
}
|
||||
|
||||
func extraInfoForPlayerError() -> String {
|
||||
return ""
|
||||
}
|
||||
|
||||
func playerStatusDidChange() {
|
||||
Logger.debug("player status: \(player?.currentItem?.status.rawValue ?? -1)")
|
||||
switch player?.currentItem?.status {
|
||||
case .readyToPlay:
|
||||
if maskProvider?.needVideoOutput() == true {
|
||||
setUpOutput()
|
||||
let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in
|
||||
if let oldPlayer = obs.oldValue, let oldPlayer {
|
||||
self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) }
|
||||
}
|
||||
startPlay()
|
||||
case .failed:
|
||||
removeObservarPlayerItem()
|
||||
Logger.debug(player?.currentItem?.error ?? "no error")
|
||||
Logger.debug(player?.currentItem?.errorLog() ?? "no error log")
|
||||
if retryCount < maxRetryCount, !retryPlay() {
|
||||
let log = playerItem?.errorLog()
|
||||
let errorLogData = log?.extendedLogData() ?? Data()
|
||||
var str = String(data: errorLogData, encoding: .utf8) ?? ""
|
||||
str = str.split(separator: "\n").dropFirst(4).joined()
|
||||
showErrorAlertAndExit(title: "播放器失败", message: str + extraInfoForPlayerError())
|
||||
}
|
||||
retryCount += 1
|
||||
default:
|
||||
break
|
||||
self?.playerDidChange(player: vc.player)
|
||||
}
|
||||
observations.insert(playerObservation)
|
||||
activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) }
|
||||
}
|
||||
|
||||
func playerRateDidChange(player: AVPlayer) {}
|
||||
|
||||
@MainActor func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?) {
|
||||
let desp = desp?.components(separatedBy: "\n").joined(separator: " ")
|
||||
let mapping: [AVMetadataIdentifier: Any?] = [
|
||||
.commonIdentifierTitle: title,
|
||||
.iTunesMetadataTrackSubTitle: subTitle,
|
||||
.commonIdentifierDescription: desp,
|
||||
]
|
||||
let meta = mapping.compactMap { createMetadataItem(for: $0, value: $1) }
|
||||
playerInfo = meta
|
||||
playerItem?.externalMetadata = meta
|
||||
|
||||
if let pic = pic {
|
||||
let resource = Kingfisher.ImageResource(downloadURL: pic)
|
||||
KingfisherManager.shared.retrieveImage(with: resource) {
|
||||
[weak self] result in
|
||||
guard let self = self,
|
||||
let data = try? result.get().image.pngData(),
|
||||
let item = self.createMetadataItem(for: .commonIdentifierArtwork, value: data)
|
||||
else { return }
|
||||
|
||||
self.playerInfo?.removeAll { $0.identifier == .commonIdentifierArtwork }
|
||||
self.playerInfo?.append(item)
|
||||
self.playerItem?.externalMetadata = self.playerInfo ?? []
|
||||
}
|
||||
}
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) }
|
||||
}
|
||||
|
||||
func createMetadataItem(for identifier: AVMetadataIdentifier,
|
||||
value: Any?) -> AVMetadataItem?
|
||||
{
|
||||
if value == nil { return nil }
|
||||
let item = AVMutableMetadataItem()
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
// Specify "und" to indicate an undefined language.
|
||||
item.extendedLanguageTag = "und"
|
||||
return item.copy() as? AVMetadataItem
|
||||
override var preferredFocusEnvironments: [UIFocusEnvironment] {
|
||||
return [playerVC.view]
|
||||
}
|
||||
|
||||
private func setupPlayerMenu() {
|
||||
var menus = [UIMenuElement]()
|
||||
let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill")
|
||||
let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle")
|
||||
let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) {
|
||||
[weak self] action in
|
||||
guard let self = self else { return }
|
||||
Settings.defaultDanmuStatus.toggle()
|
||||
self.danMuView.isHidden.toggle()
|
||||
action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage
|
||||
}
|
||||
menus.append(danmuAction)
|
||||
|
||||
let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in
|
||||
UIAction(title: "\(dur) 秒", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur }
|
||||
})
|
||||
let danmuAILevelMenu = UIMenu(title: "弹幕屏蔽等级", options: [.displayInline, .singleSelection], children: [Int32](1...10).map { level in
|
||||
UIAction(title: "\(level)", state: level == Settings.danmuAILevel ? .on : .off) { _ in Settings.danmuAILevel = level }
|
||||
})
|
||||
let danmuSettingMenu = UIMenu(title: "弹幕设置", image: UIImage(systemName: "keyboard.badge.ellipsis"), children: [danmuDurationMenu, danmuAILevelMenu])
|
||||
menus.append(danmuSettingMenu)
|
||||
|
||||
let debugEnableImage = UIImage(systemName: "terminal.fill")
|
||||
let debugDisableImage = UIImage(systemName: "terminal")
|
||||
let debugAction = UIAction(title: "Debug", image: debugEnable ? debugEnableImage : debugDisableImage) {
|
||||
[weak self] action in
|
||||
guard let self = self else { return }
|
||||
if self.debugEnable {
|
||||
self.stopDebug()
|
||||
action.image = debugDisableImage
|
||||
} else {
|
||||
action.image = debugEnableImage
|
||||
self.startDebug()
|
||||
}
|
||||
}
|
||||
|
||||
if allowChangeSpeed {
|
||||
// Create ∞ and ⚙ images.
|
||||
let loopImage = UIImage(systemName: "infinity")
|
||||
let gearImage = UIImage(systemName: "gearshape")
|
||||
|
||||
// Create an action to enable looping playback.
|
||||
let loopAction = UIAction(title: "循环播放", image: loopImage, state: Settings.loopPlay ? .on : .off) {
|
||||
action in
|
||||
action.state = (action.state == .off) ? .on : .off
|
||||
Settings.loopPlay = action.state == .on
|
||||
}
|
||||
|
||||
let speedActions = PlaySpeed.blDefaults.map { playSpeed in
|
||||
UIAction(title: playSpeed.name, state: player?.rate ?? 1 == playSpeed.value ? .on : .off) { [weak self] _ in
|
||||
self?.player?.currentItem?.audioTimePitchAlgorithm = .timeDomain
|
||||
self?.selectSpeed(AVPlaybackSpeed(rate: playSpeed.value, localizedName: playSpeed.name))
|
||||
self?.danMuView.playingSpeed = playSpeed.value
|
||||
}
|
||||
}
|
||||
let playSpeedMenu = UIMenu(title: "播放速度", options: [.displayInline, .singleSelection], children: speedActions)
|
||||
let menu = UIMenu(title: "播放设置", image: gearImage, children: [playSpeedMenu, loopAction, debugAction])
|
||||
menus.append(menu)
|
||||
} else {
|
||||
menus.append(debugAction)
|
||||
}
|
||||
|
||||
transportBarCustomMenuItems = menus
|
||||
func addPlugin(plugin: CommonPlayerPlugin) {
|
||||
plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!)
|
||||
activePlugins.append(plugin)
|
||||
plugin.playerDidLoad(playerVC: playerVC)
|
||||
}
|
||||
|
||||
private func removeObservarPlayerItem() {
|
||||
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
|
||||
func removePlugin(plugin: CommonPlayerPlugin) {
|
||||
activePlugins.removeAll { $0 == plugin }
|
||||
}
|
||||
|
||||
private func observePlayerItem(_ playerItem: AVPlayerItem) {
|
||||
observer = playerItem.observe(\.status, options: [.new, .old]) {
|
||||
[weak self] _, _ in
|
||||
self?.playerStatusDidChange()
|
||||
}
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(playerDidFinishPlaying),
|
||||
name: .AVPlayerItemDidPlayToEndTime,
|
||||
object: playerItem)
|
||||
}
|
||||
|
||||
func setupMask() {
|
||||
guard let maskProvider else { return }
|
||||
// danMuView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
|
||||
Logger.info("mask provider is \(maskProvider)")
|
||||
let interval = CMTime(seconds: 1.0 / CGFloat(maskProvider.preferFPS()),
|
||||
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: {
|
||||
[weak self, weak maskProvider] time in
|
||||
guard let self else { return }
|
||||
guard self.danMuView.isHidden == false else { return }
|
||||
maskProvider?.getMask(for: time, frame: self.danMuView.frame) {
|
||||
maskLayer in
|
||||
self.danMuView.layer.mask = maskLayer
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func retryPlay() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func playerDidFinishPlaying() {
|
||||
playDidEnd()
|
||||
}
|
||||
|
||||
func playDidEnd() {}
|
||||
func playerDidEnd(player: AVPlayer) {}
|
||||
|
||||
func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
@ -271,111 +65,74 @@ class CommonPlayerViewController: AVPlayerViewController {
|
|||
alertController.addAction(actionOk)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func startPlay() {
|
||||
guard player?.rate == 0 && player?.error == nil else { return }
|
||||
if let playerStartPos = playerStartPos {
|
||||
player?.seek(to: CMTime(seconds: Double(playerStartPos), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
extension CommonPlayerViewController {
|
||||
private func playerDidChange(player: AVPlayer?) {
|
||||
if let player {
|
||||
activePlugins.forEach { $0.playerDidChange(player: player) }
|
||||
rateObserver = player.observe(\.rate, options: [.old, .new]) {
|
||||
[weak self] _player, obs in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.playerRateDidChange(player: player)
|
||||
}
|
||||
}
|
||||
if let playItem = player.currentItem {
|
||||
observePlayerItem(playItem)
|
||||
}
|
||||
var menus = [UIMenuElement]()
|
||||
activePlugins.forEach {
|
||||
let newMenus = $0.addMenuItems(current: &menus)
|
||||
menus.append(contentsOf: newMenus)
|
||||
}
|
||||
playerVC.transportBarCustomMenuItems = menus
|
||||
} else {
|
||||
rateObserver = nil
|
||||
}
|
||||
player?.play()
|
||||
}
|
||||
|
||||
private func fetchDebugInfo() -> String {
|
||||
let bitrateStr: (Double) -> String = {
|
||||
bit in
|
||||
String(format: "%.2fMbps", bit / 1024.0 / 1024.0)
|
||||
}
|
||||
guard let player else { return "Player no init" }
|
||||
|
||||
var logs = """
|
||||
\(additionDebugInfo())
|
||||
time control status: \(player.timeControlStatus.rawValue) \(player.reasonForWaitingToPlay?.rawValue ?? "")
|
||||
player status:\(player.status.rawValue)
|
||||
"""
|
||||
|
||||
guard let log = player.currentItem?.accessLog() else { return logs }
|
||||
guard let item = log.events.last else { return logs }
|
||||
let uri = item.uri ?? ""
|
||||
let addr = item.serverAddress ?? ""
|
||||
let changes = item.numberOfServerAddressChanges
|
||||
let dropped = item.numberOfDroppedVideoFrames
|
||||
let stalls = item.numberOfStalls
|
||||
let averageAudioBitrate = item.averageAudioBitrate
|
||||
let averageVideoBitrate = item.averageVideoBitrate
|
||||
let indicatedBitrate = item.indicatedBitrate
|
||||
let observedBitrate = item.observedBitrate
|
||||
logs += """
|
||||
uri:\(uri), ip:\(addr), change:\(changes)
|
||||
drop:\(dropped) stalls:\(stalls)
|
||||
bitrate audio:\(bitrateStr(averageAudioBitrate)), video: \(bitrateStr(averageVideoBitrate))
|
||||
observedBitrate:\(bitrateStr(observedBitrate))
|
||||
indicatedAverageBitrate:\(bitrateStr(indicatedBitrate))
|
||||
maskProvider: \(String(describing: maskProvider))
|
||||
"""
|
||||
return logs
|
||||
}
|
||||
|
||||
func additionDebugInfo() -> String { return "" }
|
||||
|
||||
var debugTimer: Timer?
|
||||
var debugEnable: Bool { debugTimer?.isValid ?? false }
|
||||
private func startDebug() {
|
||||
if debugView == nil {
|
||||
debugView = UILabel()
|
||||
debugView?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
|
||||
debugView?.textColor = UIColor.white
|
||||
view.addSubview(debugView!)
|
||||
debugView?.numberOfLines = 0
|
||||
debugView?.font = UIFont.systemFont(ofSize: 26)
|
||||
debugView?.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(12)
|
||||
make.right.equalToSuperview().offset(-12)
|
||||
make.width.equalTo(800)
|
||||
private func playerRateDidChange(player: AVPlayer) {
|
||||
if player.rate > 0 {
|
||||
activePlugins.forEach { $0.playerDidStart(player: player) }
|
||||
} else if player.rate == 0 {
|
||||
if !isEnd {
|
||||
activePlugins.forEach { $0.playerDidPause(player: player) }
|
||||
}
|
||||
}
|
||||
debugView?.isHidden = false
|
||||
debugTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
let info = self?.fetchDebugInfo()
|
||||
self?.debugView?.text = info
|
||||
}
|
||||
|
||||
private func observePlayerItem(_ playerItem: AVPlayerItem) {
|
||||
statusObserver = playerItem.observe(\.status, options: [.new, .old]) {
|
||||
[weak self] item, _ in
|
||||
guard let self, let player = playerVC.player else { return }
|
||||
switch item.status {
|
||||
case .readyToPlay:
|
||||
isEnd = false
|
||||
activePlugins.forEach { $0.playerWillStart(player: player) }
|
||||
player.play()
|
||||
case .failed:
|
||||
activePlugins.forEach { $0.playerDidFail(player: player) }
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
|
||||
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in
|
||||
guard let self, let player = playerVC.player else { return }
|
||||
isEnd = true
|
||||
activePlugins.forEach { $0.playerDidEnd(player: player) }
|
||||
playerDidEnd(player: player)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopDebug() {
|
||||
debugTimer?.invalidate()
|
||||
debugTimer = nil
|
||||
debugView?.isHidden = true
|
||||
}
|
||||
|
||||
private func initDanmuView() {
|
||||
view.addSubview(danMuView)
|
||||
danMuView.accessibilityLabel = "danmuView"
|
||||
danMuView.makeConstraintsToBindToSuperview()
|
||||
danMuView.isHidden = !Settings.defaultDanmuStatus
|
||||
}
|
||||
|
||||
func setUpOutput() {
|
||||
guard videoOutput == nil, let videoItem = player?.currentItem else { return }
|
||||
let pixelBuffAttributes = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
]
|
||||
let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBuffAttributes)
|
||||
videoItem.add(videoOutput)
|
||||
self.videoOutput = videoOutput
|
||||
maskProvider?.setVideoOutout(ouput: videoOutput)
|
||||
}
|
||||
|
||||
func ensureDanmuViewFront() {
|
||||
view.bringSubviewToFront(danMuView)
|
||||
danMuView.play()
|
||||
}
|
||||
}
|
||||
|
||||
extension CommonPlayerViewController: AVPlayerViewControllerDelegate {
|
||||
@objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool {
|
||||
if let presentedViewController = UIViewController.topMostViewController() as? AVPlayerViewController,
|
||||
presentedViewController == playerViewController
|
||||
if let presentedViewController = UIViewController.topMostViewController() as? CommonPlayerViewController,
|
||||
presentedViewController.playerVC == playerViewController
|
||||
{
|
||||
return true
|
||||
dismiss(animated: true)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -384,40 +141,37 @@ extension CommonPlayerViewController: AVPlayerViewControllerDelegate {
|
|||
return true
|
||||
}
|
||||
|
||||
func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
||||
PipRecorder.shared.playingPipViewController.append(self)
|
||||
}
|
||||
|
||||
func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
||||
PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController }
|
||||
}
|
||||
|
||||
@objc func playerViewController(_ playerViewController: AVPlayerViewController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
let presentedViewController = UIViewController.topMostViewController()
|
||||
if presentedViewController is AVPlayerViewController {
|
||||
guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else {
|
||||
completionHandler(false)
|
||||
return
|
||||
}
|
||||
if presentedViewController is CommonPlayerViewController {
|
||||
let parent = presentedViewController.presentingViewController
|
||||
presentedViewController.dismiss(animated: false) {
|
||||
parent?.present(playerViewController, animated: false)
|
||||
parent?.present(containerPlayer, animated: false)
|
||||
completionHandler(true)
|
||||
(playerViewController as? CommonPlayerViewController)?.ensureDanmuViewFront()
|
||||
}
|
||||
} else {
|
||||
presentedViewController.present(playerViewController, animated: false) {
|
||||
presentedViewController.present(containerPlayer, animated: false) {
|
||||
completionHandler(true)
|
||||
(playerViewController as? CommonPlayerViewController)?.ensureDanmuViewFront()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaySpeed {
|
||||
var name: String
|
||||
var value: Float
|
||||
}
|
||||
|
||||
extension PlaySpeed: Equatable {
|
||||
static let `default` = PlaySpeed(name: "1X", value: 1)
|
||||
|
||||
static let blDefaults = [
|
||||
PlaySpeed(name: "0.5X", value: 0.5),
|
||||
PlaySpeed(name: "0.75X", value: 0.75),
|
||||
PlaySpeed(name: "1X", value: 1),
|
||||
PlaySpeed(name: "1.25X", value: 1.25),
|
||||
PlaySpeed(name: "1.5X", value: 1.5),
|
||||
PlaySpeed(name: "2X", value: 2),
|
||||
]
|
||||
class PipRecorder {
|
||||
static let shared = PipRecorder()
|
||||
var playingPipViewController = [CommonPlayerViewController]()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
//
|
||||
// NewCommonPlayerViewController.swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by yicheng on 2024/5/23.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import UIKit
|
||||
|
||||
class NewCommonPlayerViewController: UIViewController {
|
||||
private let playerVC = AVPlayerViewController()
|
||||
private var activePlugins = [CommonPlayerPlugin]()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
private var rateObserver: NSKeyValueObservation?
|
||||
private var statusObserver: NSKeyValueObservation?
|
||||
private var isEnd = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
addChild(playerVC)
|
||||
view.addSubview(playerVC.view)
|
||||
playerVC.didMove(toParent: self)
|
||||
playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() }
|
||||
playerVC.allowsPictureInPicturePlayback = true
|
||||
playerVC.delegate = self
|
||||
|
||||
let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in
|
||||
if let oldPlayer = obs.oldValue, let oldPlayer {
|
||||
self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) }
|
||||
}
|
||||
self?.playerDidChange(player: vc.player)
|
||||
}
|
||||
observations.insert(playerObservation)
|
||||
activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) }
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) }
|
||||
}
|
||||
|
||||
override var preferredFocusEnvironments: [UIFocusEnvironment] {
|
||||
return [playerVC.view]
|
||||
}
|
||||
|
||||
func addPlugin(plugin: CommonPlayerPlugin) {
|
||||
plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!)
|
||||
activePlugins.append(plugin)
|
||||
plugin.playerDidLoad(playerVC: playerVC)
|
||||
}
|
||||
|
||||
func removePlugin(plugin: CommonPlayerPlugin) {
|
||||
activePlugins.removeAll { $0 == plugin }
|
||||
}
|
||||
|
||||
func playerDidEnd(player: AVPlayer) {}
|
||||
|
||||
func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
let actionOk = UIAlertAction(title: "OK", style: .default) {
|
||||
[weak self] _ in
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
alertController.addAction(actionOk)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension NewCommonPlayerViewController {
|
||||
private func playerDidChange(player: AVPlayer?) {
|
||||
if let player {
|
||||
activePlugins.forEach { $0.playerDidChange(player: player) }
|
||||
rateObserver = player.observe(\.rate, options: [.old, .new]) {
|
||||
[weak self] _player, obs in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.playerRateDidChange(player: player)
|
||||
}
|
||||
}
|
||||
if let playItem = player.currentItem {
|
||||
observePlayerItem(playItem)
|
||||
}
|
||||
var menus = [UIMenuElement]()
|
||||
activePlugins.forEach {
|
||||
let newMenus = $0.addMenuItems(current: &menus)
|
||||
menus.append(contentsOf: newMenus)
|
||||
}
|
||||
playerVC.transportBarCustomMenuItems = menus
|
||||
} else {
|
||||
rateObserver = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func playerRateDidChange(player: AVPlayer) {
|
||||
if player.rate > 0 {
|
||||
activePlugins.forEach { $0.playerDidStart(player: player) }
|
||||
} else if player.rate == 0 {
|
||||
if !isEnd {
|
||||
activePlugins.forEach { $0.playerDidPause(player: player) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func observePlayerItem(_ playerItem: AVPlayerItem) {
|
||||
statusObserver = playerItem.observe(\.status, options: [.new, .old]) {
|
||||
[weak self] item, _ in
|
||||
guard let self, let player = playerVC.player else { return }
|
||||
switch item.status {
|
||||
case .readyToPlay:
|
||||
isEnd = false
|
||||
activePlugins.forEach { $0.playerWillStart(player: player) }
|
||||
player.play()
|
||||
case .failed:
|
||||
activePlugins.forEach { $0.playerDidFail(player: player) }
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
|
||||
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in
|
||||
guard let self, let player = playerVC.player else { return }
|
||||
isEnd = true
|
||||
activePlugins.forEach { $0.playerDidEnd(player: player) }
|
||||
playerDidEnd(player: player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate {
|
||||
@objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool {
|
||||
if let presentedViewController = UIViewController.topMostViewController() as? NewCommonPlayerViewController,
|
||||
presentedViewController.playerVC == playerViewController
|
||||
{
|
||||
dismiss(animated: true)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
||||
PipRecorder.shared.playingPipViewController.append(self)
|
||||
}
|
||||
|
||||
func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
||||
PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController }
|
||||
}
|
||||
|
||||
@objc func playerViewController(_ playerViewController: AVPlayerViewController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
let presentedViewController = UIViewController.topMostViewController()
|
||||
guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else {
|
||||
completionHandler(false)
|
||||
return
|
||||
}
|
||||
if presentedViewController is NewCommonPlayerViewController {
|
||||
let parent = presentedViewController.presentingViewController
|
||||
presentedViewController.dismiss(animated: false) {
|
||||
parent?.present(containerPlayer, animated: false)
|
||||
completionHandler(true)
|
||||
}
|
||||
} else {
|
||||
presentedViewController.present(containerPlayer, animated: false) {
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PipRecorder {
|
||||
static let shared = PipRecorder()
|
||||
var playingPipViewController = [NewCommonPlayerViewController]()
|
||||
}
|
||||
}
|
|
@ -16,10 +16,6 @@ protocol DanmuProviderProtocol {
|
|||
}
|
||||
|
||||
class DanmuViewPlugin: NSObject {
|
||||
var showDanmu = Settings.defaultDanmuStatus {
|
||||
didSet { danMuView.isHidden = !showDanmu }
|
||||
}
|
||||
|
||||
let danMuView = DanmakuView()
|
||||
|
||||
init(provider: DanmuProviderProtocol) {
|
||||
|
@ -30,6 +26,13 @@ class DanmuViewPlugin: NSObject {
|
|||
.sink { [weak self] in
|
||||
self?.shoot($0)
|
||||
}.store(in: &cancellable)
|
||||
|
||||
Defaults.shared.$showDanmu
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink {
|
||||
[weak self] in
|
||||
self?.danMuView.isHidden = !$0
|
||||
}.store(in: &cancellable)
|
||||
}
|
||||
|
||||
private let danmuProvider: DanmuProviderProtocol
|
||||
|
@ -49,7 +52,7 @@ extension DanmuViewPlugin: CommonPlayerPlugin {
|
|||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1),
|
||||
queue: DispatchQueue.global()) { [weak self] time in
|
||||
guard let self else { return }
|
||||
if danMuView.isHidden { return }
|
||||
if !Defaults.shared.showDanmu { return }
|
||||
let seconds = time.seconds
|
||||
danmuProvider.playerTimeChange(time: seconds)
|
||||
}
|
||||
|
@ -66,7 +69,6 @@ extension DanmuViewPlugin: CommonPlayerPlugin {
|
|||
danMuView.makeConstraintsToBindToSuperview()
|
||||
danMuView.setNeedsLayout()
|
||||
danMuView.layoutIfNeeded()
|
||||
danMuView.isHidden = showDanmu
|
||||
danMuView.paddingTop = 5
|
||||
danMuView.trackHeight = 50
|
||||
danMuView.displayArea = Settings.danmuArea.percent
|
||||
|
@ -85,11 +87,9 @@ extension DanmuViewPlugin: CommonPlayerPlugin {
|
|||
let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill")
|
||||
let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle")
|
||||
let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) {
|
||||
[weak self] action in
|
||||
guard let self = self else { return }
|
||||
Settings.defaultDanmuStatus.toggle()
|
||||
self.danMuView.isHidden.toggle()
|
||||
action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage
|
||||
action in
|
||||
Defaults.shared.showDanmu.toggle()
|
||||
action.image = Defaults.shared.showDanmu ? danmuImage : danmuImageDisable
|
||||
}
|
||||
let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in
|
||||
UIAction(title: "\(dur) 秒", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur }
|
|
@ -42,3 +42,21 @@ class SpeedChangerPlugin: NSObject, CommonPlayerPlugin {
|
|||
return [menu]
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaySpeed {
|
||||
var name: String
|
||||
var value: Float
|
||||
}
|
||||
|
||||
extension PlaySpeed: Equatable {
|
||||
static let `default` = PlaySpeed(name: "1X", value: 1)
|
||||
|
||||
static let blDefaults = [
|
||||
PlaySpeed(name: "0.5X", value: 0.5),
|
||||
PlaySpeed(name: "0.75X", value: 0.75),
|
||||
PlaySpeed(name: "1X", value: 1),
|
||||
PlaySpeed(name: "1.25X", value: 1.25),
|
||||
PlaySpeed(name: "1.5X", value: 1.5),
|
||||
PlaySpeed(name: "2X", value: 2),
|
||||
]
|
||||
}
|
|
@ -5,7 +5,9 @@
|
|||
// Created by whw on 2022/10/19.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum FeedDisplayStyle: Codable, CaseIterable {
|
||||
case large
|
||||
|
@ -17,6 +19,13 @@ enum FeedDisplayStyle: Codable, CaseIterable {
|
|||
}
|
||||
}
|
||||
|
||||
class Defaults {
|
||||
static let shared = Defaults()
|
||||
private init() {}
|
||||
|
||||
@Published(key: "Settings.danmuStatus") var showDanmu = true
|
||||
}
|
||||
|
||||
enum Settings {
|
||||
@UserDefaultCodable("Settings.displayStyle", defaultValue: .normal)
|
||||
static var displayStyle: FeedDisplayStyle
|
||||
|
@ -45,9 +54,6 @@ enum Settings {
|
|||
@UserDefault("Settings.preferAvc", defaultValue: false)
|
||||
static var preferAvc: Bool
|
||||
|
||||
@UserDefault("Settings.defaultDanmuStatus", defaultValue: true)
|
||||
static var defaultDanmuStatus: Bool
|
||||
|
||||
@UserDefault("Settings.danmuMask", defaultValue: true)
|
||||
static var danmuMask: Bool
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// MaskProvider.swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by yicheng on 2024/6/10.
|
||||
//
|
||||
import AVKit
|
||||
|
||||
protocol MaskProvider: AnyObject {
|
||||
func getMask(for time: CMTime, frame: CGRect, onGet: @escaping (CALayer) -> Void)
|
||||
func needVideoOutput() -> Bool
|
||||
func setVideoOutout(ouput: AVPlayerItemVideoOutput)
|
||||
func preferFPS() -> Int
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
//
|
||||
// NewVideoPlayerViewController.swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by yicheng on 2024/5/23.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
class NewVideoPlayerViewController: NewCommonPlayerViewController {
|
||||
var data: VideoDetail?
|
||||
var nextProvider: VideoNextProvider?
|
||||
|
||||
init(playInfo: PlayInfo) {
|
||||
viewModel = NewVideoPlayerViewModel(playInfo: playInfo)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private let viewModel: NewVideoPlayerViewModel
|
||||
private var cancelable = Set<AnyCancellable>()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
viewModel.nextProvider = nextProvider
|
||||
viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in
|
||||
switch completion {
|
||||
case let .failure(err):
|
||||
self?.showErrorAlertAndExit(message: err)
|
||||
default:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] plugins in
|
||||
plugins.forEach { self?.addPlugin(plugin: $0) }
|
||||
}.store(in: &cancelable)
|
||||
viewModel.onPluginRemove.sink { [weak self] in
|
||||
self?.removePlugin(plugin: $0)
|
||||
}.store(in: &cancelable)
|
||||
viewModel.onExit = { [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
Task {
|
||||
await viewModel.load()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ struct PlayerDetailData {
|
|||
var videoPlayURLInfo: VideoPlayURLInfo
|
||||
}
|
||||
|
||||
class NewVideoPlayerViewModel {
|
||||
class VideoPlayerViewModel {
|
||||
var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>()
|
||||
var onPluginRemove = PassthroughSubject<CommonPlayerPlugin, Never>()
|
||||
var onExit: (() -> Void)?
|
||||
|
@ -78,7 +78,19 @@ class NewVideoPlayerViewModel {
|
|||
var clipInfos: [VideoPlayURLInfo.ClipInfo]?
|
||||
|
||||
if playInfo.isBangumi {
|
||||
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
|
||||
do {
|
||||
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
|
||||
} catch let err as RequestError {
|
||||
if case let .statusFail(code, _) = err,
|
||||
code == -404 || code == -10403,
|
||||
let data = try await fetchAreaLimitPcgVideoData()
|
||||
{
|
||||
playData = data
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
clipInfos = playData.clip_info_list
|
||||
} else {
|
||||
playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid)
|
||||
|
@ -97,15 +109,6 @@ class NewVideoPlayerViewModel {
|
|||
|
||||
} catch let err {
|
||||
if case let .statusFail(code, message) = err as? RequestError {
|
||||
if code == -404 || code == -10403 {
|
||||
// 解锁港澳台番剧处理
|
||||
// do {
|
||||
// if let ok = try await fetchAreaLimitVideoData(), ok {
|
||||
// return
|
||||
// }
|
||||
// } catch let err {
|
||||
// }
|
||||
}
|
||||
throw "\(code) \(message),可能需要大会员"
|
||||
} else if await infoReq?.is_upower_exclusive == true {
|
||||
throw "该视频为充电专属视频 \(err)"
|
||||
|
@ -181,3 +184,56 @@ class NewVideoPlayerViewModel {
|
|||
return plugins
|
||||
}
|
||||
}
|
||||
|
||||
// 港澳台解锁
|
||||
extension VideoPlayerViewModel {
|
||||
private func fetchAreaLimitPcgVideoData() async throws -> VideoPlayURLInfo? {
|
||||
guard Settings.areaLimitUnlock else { return nil }
|
||||
guard let epid = playInfo.epid, epid > 0 else { return nil }
|
||||
|
||||
let season = try await WebRequest.requestBangumiSeasonView(epid: epid)
|
||||
let checkTitle = season.title.contains("僅") ? season.title : season.series_title
|
||||
let checkAreaList = parseAreaByTitle(title: checkTitle)
|
||||
guard !checkAreaList.isEmpty else { return nil }
|
||||
|
||||
let playData = try await requestAreaLimitPcgPlayUrl(epid: epid, cid: playInfo.cid!, areaList: checkAreaList)
|
||||
return playData
|
||||
}
|
||||
|
||||
private func requestAreaLimitPcgPlayUrl(epid: Int, cid: Int, areaList: [String]) async throws -> VideoPlayURLInfo? {
|
||||
for area in areaList {
|
||||
do {
|
||||
return try await WebRequest.requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, area: area)
|
||||
} catch let err {
|
||||
if area == areaList.last {
|
||||
throw err
|
||||
} else {
|
||||
print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func parseAreaByTitle(title: String) -> [String] {
|
||||
if title.isMatch(pattern: "[仅|僅].*[东南亚|其他]") {
|
||||
// TODO: 未支持
|
||||
return []
|
||||
}
|
||||
|
||||
var areas: [String] = []
|
||||
if title.isMatch(pattern: "僅.*台") {
|
||||
areas.append("tw")
|
||||
}
|
||||
if title.isMatch(pattern: "僅.*港") {
|
||||
areas.append("hk")
|
||||
}
|
||||
|
||||
if areas.isEmpty {
|
||||
// 标题没有地区限制信息,返回尝试检测的区域
|
||||
return ["tw", "hk"]
|
||||
} else {
|
||||
return areas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,26 @@ import AVFoundation
|
|||
import Foundation
|
||||
class BUpnpPlugin: NSObject, CommonPlayerPlugin {
|
||||
let duration: Int?
|
||||
weak var player: AVPlayer?
|
||||
|
||||
init(duration: Int?) {
|
||||
self.duration = duration
|
||||
}
|
||||
|
||||
func pause() {
|
||||
player?.pause()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
player?.play()
|
||||
}
|
||||
|
||||
func seek(to time: TimeInterval) {
|
||||
player?.seek(to: CMTime(seconds: time, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
|
||||
func playerWillStart(player: AVPlayer) {
|
||||
self.player = player
|
||||
guard let duration else { return }
|
||||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 5, preferredTimescale: 1), queue: .global()) { time in
|
||||
DispatchQueue.main.async {
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// Created by Yam on 2024/6/9.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ReplyDetailViewController: UIViewController {
|
||||
private var titleLabel: UILabel!
|
||||
private var replyLabel: UILabel!
|
||||
private var replyCollectionView: UICollectionView!
|
||||
|
||||
var reply: Replys.Reply
|
||||
|
||||
init(reply: Replys.Reply) {
|
||||
self.reply = reply
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setUpViews()
|
||||
replyLabel.text = reply.content.message
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setUpViews() {
|
||||
titleLabel = {
|
||||
let label = UILabel()
|
||||
self.view.addSubview(label)
|
||||
label.font = .boldSystemFont(ofSize: 60)
|
||||
label.text = "评论"
|
||||
|
||||
label.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
replyLabel = {
|
||||
let label = UILabel()
|
||||
self.view.addSubview(label)
|
||||
label.numberOfLines = 0
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
|
||||
label.snp.makeConstraints { make in
|
||||
make.top.equalTo(self.titleLabel.snp.bottom).offset(60)
|
||||
make.leading.equalTo(self.view.snp.leadingMargin)
|
||||
make.trailing.equalTo(self.view.snp.trailingMargin)
|
||||
}
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
replyCollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.itemSize = CGSize(width: 582, height: 360)
|
||||
flowLayout.sectionInset = .init(top: 0, left: 60, bottom: 0, right: 60)
|
||||
flowLayout.minimumLineSpacing = 10
|
||||
flowLayout.minimumInteritemSpacing = 10
|
||||
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
self.view.addSubview(collectionView)
|
||||
collectionView.dataSource = self
|
||||
collectionView.delegate = self
|
||||
collectionView.register(UINib(nibName: ReplyCell.identifier, bundle: nil), forCellWithReuseIdentifier: ReplyCell.identifier)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.top.equalTo(self.replyLabel.snp.bottom).offset(60)
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
|
||||
return collectionView
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
extension ReplyDetailViewController: UICollectionViewDataSource, UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return reply.replies?.count ?? 0
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReplyCell.identifier, for: indexPath) as? ReplyCell else {
|
||||
fatalError("cell not found")
|
||||
}
|
||||
|
||||
guard let reply = reply.replies?[indexPath.row] else {
|
||||
fatalError("reply not found")
|
||||
}
|
||||
|
||||
cell.config(replay: reply)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let reply = reply.replies?[indexPath.item] else { return }
|
||||
let detail = ReplyDetailViewController(reply: reply)
|
||||
present(detail, animated: true)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import AVKit
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
|
@ -43,6 +44,7 @@ class VideoDetailViewController: UIViewController {
|
|||
@IBOutlet var pageCollectionView: UICollectionView!
|
||||
@IBOutlet var recommandCollectionView: UICollectionView!
|
||||
@IBOutlet var replysCollectionView: UICollectionView!
|
||||
@IBOutlet var repliesCollectionViewHeightConstraints: NSLayoutConstraint!
|
||||
@IBOutlet var ugcCollectionView: UICollectionView!
|
||||
|
||||
@IBOutlet var pageView: UIView!
|
||||
|
@ -71,6 +73,8 @@ class VideoDetailViewController: UIViewController {
|
|||
|
||||
private var allUgcEpisodes = [VideoDetail.Info.UgcSeason.UgcVideoInfo]()
|
||||
|
||||
private var subscriptions = [AnyCancellable]()
|
||||
|
||||
static func create(aid: Int, cid: Int?, epid: Int? = nil) -> VideoDetailViewController {
|
||||
let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! VideoDetailViewController
|
||||
vc.aid = aid
|
||||
|
@ -126,6 +130,11 @@ class VideoDetailViewController: UIViewController {
|
|||
focusGuide.bottomAnchor.constraint(equalTo: actionButtonSpaceView.bottomAnchor),
|
||||
])
|
||||
focusGuide.preferredFocusEnvironments = [dislikeButton]
|
||||
|
||||
replysCollectionView.publisher(for: \.contentSize).sink { [weak self] newSize in
|
||||
self?.repliesCollectionViewHeightConstraints.constant = newSize.height
|
||||
self?.view.setNeedsLayout()
|
||||
}.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
override var preferredFocusedView: UIView? {
|
||||
|
@ -147,7 +156,7 @@ class VideoDetailViewController: UIViewController {
|
|||
} else {
|
||||
vc.present(self, animated: false) { [weak self] in
|
||||
guard let self else { return }
|
||||
let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi))
|
||||
let player = VideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi))
|
||||
self.present(player, animated: true)
|
||||
}
|
||||
}
|
||||
|
@ -354,7 +363,7 @@ class VideoDetailViewController: UIViewController {
|
|||
}
|
||||
|
||||
@IBAction func actionPlay(_ sender: Any) {
|
||||
let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi))
|
||||
let player = VideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi))
|
||||
player.data = data
|
||||
if pages.count > 0, let index = pages.firstIndex(where: { $0.cid == cid }) {
|
||||
let seq = pages.dropFirst(index).map({ PlayInfo(aid: aid, cid: $0.cid, epid: $0.epid, isBangumi: isBangumi) })
|
||||
|
@ -456,7 +465,7 @@ extension VideoDetailViewController: UICollectionViewDelegate {
|
|||
present(player, animated: true, completion: nil)
|
||||
case replysCollectionView:
|
||||
guard let reply = replys?.replies?[indexPath.item] else { return }
|
||||
let detail = ContentDetailViewController.createReply(content: reply.content.message)
|
||||
let detail = ReplyDetailViewController(reply: reply)
|
||||
present(detail, animated: true)
|
||||
case ugcCollectionView:
|
||||
let video = allUgcEpisodes[indexPath.item]
|
||||
|
@ -576,18 +585,6 @@ extension VideoDetailViewController {
|
|||
}
|
||||
}
|
||||
|
||||
class ReplyCell: UICollectionViewCell {
|
||||
@IBOutlet var avatarImageView: UIImageView!
|
||||
@IBOutlet var userNameLabel: UILabel!
|
||||
@IBOutlet var contenLabel: UILabel!
|
||||
|
||||
func config(replay: Replys.Reply) {
|
||||
avatarImageView.kf.setImage(with: URL(string: replay.member.avatar), options: [.processor(DownsamplingImageProcessor(size: CGSize(width: 80, height: 80))), .processor(RoundCornerImageProcessor(radius: .widthFraction(0.5))), .cacheSerializer(FormatIndicatedCacheSerializer.png)])
|
||||
userNameLabel.text = replay.member.uname
|
||||
contenLabel.text = replay.content.message
|
||||
}
|
||||
}
|
||||
|
||||
class RelatedVideoCell: BLMotionCollectionViewCell {
|
||||
let titleLabel = MarqueeLabel()
|
||||
let imageView = UIImageView()
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
//
|
||||
// VideoPlayerViewController.swift
|
||||
// NewVideoPlayerViewController.swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by Etan Chen on 2021/4/4.
|
||||
// Created by yicheng on 2024/5/23.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
import SwiftyJSON
|
||||
import SwiftyXMLParser
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
struct PlayInfo {
|
||||
|
@ -46,9 +42,11 @@ class VideoNextProvider {
|
|||
}
|
||||
|
||||
class VideoPlayerViewController: CommonPlayerViewController {
|
||||
var playInfo: PlayInfo
|
||||
var data: VideoDetail?
|
||||
var nextProvider: VideoNextProvider?
|
||||
|
||||
init(playInfo: PlayInfo) {
|
||||
self.playInfo = playInfo
|
||||
viewModel = VideoPlayerViewModel(playInfo: playInfo)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
|
@ -57,389 +55,30 @@ class VideoPlayerViewController: CommonPlayerViewController {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var data: VideoDetail?
|
||||
var nextProvider: VideoNextProvider?
|
||||
private var allDanmus = [Danmu]()
|
||||
private var playingDanmus = [Danmu]()
|
||||
private var playerDelegate: BilibiliVideoResourceLoaderDelegate?
|
||||
private let danmuProvider = VideoDanmuProvider()
|
||||
private var clipInfos: [VideoPlayURLInfo.ClipInfo]?
|
||||
private var skipAction: UIAction?
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
guard let currentTime = player?.currentTime().seconds, currentTime > 0 else { return }
|
||||
|
||||
if let cid = playInfo.cid, cid > 0 {
|
||||
WebRequest.reportWatchHistory(aid: playInfo.aid, cid: cid, currentTime: Int(currentTime))
|
||||
}
|
||||
BiliBiliUpnpDMR.shared.sendStatus(status: .stop)
|
||||
}
|
||||
private let viewModel: VideoPlayerViewModel
|
||||
private var cancelable = Set<AnyCancellable>()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
viewModel.nextProvider = nextProvider
|
||||
viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in
|
||||
switch completion {
|
||||
case let .failure(err):
|
||||
self?.showErrorAlertAndExit(message: err)
|
||||
default:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] plugins in
|
||||
plugins.forEach { self?.addPlugin(plugin: $0) }
|
||||
}.store(in: &cancelable)
|
||||
viewModel.onPluginRemove.sink { [weak self] in
|
||||
self?.removePlugin(plugin: $0)
|
||||
}.store(in: &cancelable)
|
||||
viewModel.onExit = { [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
Task {
|
||||
await initPlayer()
|
||||
}
|
||||
danmuProvider.onShowDanmu = {
|
||||
[weak self] in
|
||||
self?.danMuView.shoot(danmaku: $0)
|
||||
}
|
||||
}
|
||||
|
||||
private func initPlayer() async {
|
||||
if !playInfo.isCidVaild {
|
||||
do {
|
||||
playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid)
|
||||
} catch let err {
|
||||
self.showErrorAlertAndExit(message: "请求cid失败,\(err.localizedDescription)")
|
||||
}
|
||||
}
|
||||
await fetchVideoData()
|
||||
await danmuProvider.initVideo(cid: playInfo.cid!, startPos: playerStartPos ?? 0)
|
||||
}
|
||||
|
||||
private func playmedia(urlInfo: VideoPlayURLInfo, playerInfo: PlayerInfo?) async {
|
||||
let playURL = URL(string: BilibiliVideoResourceLoaderDelegate.URLs.play)!
|
||||
let headers: [String: String] = [
|
||||
"User-Agent": Keys.userAgent,
|
||||
"Referer": "https://www.bilibili.com/video/av\(playInfo.aid)",
|
||||
]
|
||||
let asset = AVURLAsset(url: playURL, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
||||
playerDelegate = BilibiliVideoResourceLoaderDelegate()
|
||||
playerDelegate?.setBilibili(info: urlInfo, subtitles: playerInfo?.subtitle?.subtitles ?? [], aid: playInfo.aid)
|
||||
if Settings.contentMatchOnlyInHDR {
|
||||
if playerDelegate?.isHDR != true {
|
||||
appliesPreferredDisplayCriteriaAutomatically = false
|
||||
}
|
||||
}
|
||||
asset.resourceLoader.setDelegate(playerDelegate, queue: DispatchQueue(label: "loader"))
|
||||
let requestedKeys = ["playable"]
|
||||
await asset.loadValues(forKeys: requestedKeys)
|
||||
prepare(toPlay: asset, withKeys: requestedKeys)
|
||||
updatePlayerCharpter(playerInfo: playerInfo)
|
||||
BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0)
|
||||
}
|
||||
|
||||
private func updatePlayerCharpter(playerInfo: PlayerInfo?) {
|
||||
let group = DispatchGroup()
|
||||
var metas = [AVTimedMetadataGroup]()
|
||||
for viewPoint in playerInfo?.view_points ?? [] {
|
||||
group.enter()
|
||||
convertTimedMetadataGroup(viewPoint: viewPoint) {
|
||||
metas.append($0)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.notify(queue: .main) {
|
||||
if metas.count > 0 {
|
||||
self.playerItem?.navigationMarkerGroups = [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metas)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func extraInfoForPlayerError() -> String {
|
||||
return playerDelegate?.infoDebugText ?? "-"
|
||||
}
|
||||
|
||||
override func additionDebugInfo() -> String {
|
||||
if let port = playerDelegate?.httpPort.string() {
|
||||
return " :" + port
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
override func playerStatusDidChange() {
|
||||
super.playerStatusDidChange()
|
||||
switch player?.status {
|
||||
case .readyToPlay:
|
||||
BiliBiliUpnpDMR.shared.sendStatus(status: .playing)
|
||||
case .failed:
|
||||
BiliBiliUpnpDMR.shared.sendStatus(status: .stop)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// override func playerRateDidChange(player: AVPlayer) {
|
||||
// if player.rate > 0, danMuView.status == .pause {
|
||||
// danMuView.play()
|
||||
// } else if player.rate == 0, danMuView.status == .play {
|
||||
// danMuView.pause()
|
||||
// }
|
||||
// }
|
||||
|
||||
func playNext() -> Bool {
|
||||
if let next = nextProvider?.getNext() {
|
||||
playInfo = next
|
||||
playerStartPos = 0
|
||||
Task {
|
||||
await initPlayer()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func playDidEnd() {
|
||||
BiliBiliUpnpDMR.shared.sendStatus(status: .end)
|
||||
if !playNext() {
|
||||
if Settings.loopPlay {
|
||||
nextProvider?.reset()
|
||||
if !playNext() {
|
||||
playerItem?.seek(to: .zero, completionHandler: nil)
|
||||
player?.play()
|
||||
}
|
||||
return
|
||||
}
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func convertTimedMetadataGroup(viewPoint: PlayerInfo.ViewPoint, onResult: ((AVTimedMetadataGroup) -> Void)? = nil) {
|
||||
let mapping: [AVMetadataIdentifier: Any?] = [
|
||||
.commonIdentifierTitle: viewPoint.content,
|
||||
]
|
||||
|
||||
var metadatas = mapping.compactMap { createMetadataItem(for: $0, value: $1) }
|
||||
let timescale: Int32 = 600
|
||||
let cmStartTime = CMTimeMakeWithSeconds(viewPoint.from, preferredTimescale: timescale)
|
||||
let cmEndTime = CMTimeMakeWithSeconds(viewPoint.to, preferredTimescale: timescale)
|
||||
let timeRange = CMTimeRangeFromTimeToTime(start: cmStartTime, end: cmEndTime)
|
||||
if let pic = viewPoint.imgUrl?.addSchemeIfNeed() {
|
||||
let resource = Kingfisher.ImageResource(downloadURL: pic)
|
||||
KingfisherManager.shared.retrieveImage(with: resource) {
|
||||
[weak self] result in
|
||||
guard let self = self,
|
||||
let data = try? result.get().image.pngData(),
|
||||
let item = self.createMetadataItem(for: .commonIdentifierArtwork, value: data)
|
||||
else {
|
||||
onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange))
|
||||
return
|
||||
}
|
||||
metadatas.append(item)
|
||||
onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange))
|
||||
}
|
||||
} else {
|
||||
onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Requests
|
||||
|
||||
extension VideoPlayerViewController {
|
||||
func fetchVideoData() async {
|
||||
assert(playInfo.isCidVaild)
|
||||
let aid = playInfo.aid
|
||||
let cid = playInfo.cid!
|
||||
let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid)
|
||||
do {
|
||||
let playData: VideoPlayURLInfo
|
||||
if playInfo.isBangumi {
|
||||
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
|
||||
clipInfos = playData.clip_info_list
|
||||
} else {
|
||||
playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid)
|
||||
}
|
||||
if info?.last_play_cid == cid, let startTime = info?.playTimeInSecond, playData.dash.duration - startTime > 5, Settings.continuePlay {
|
||||
playerStartPos = startTime
|
||||
}
|
||||
|
||||
await playmedia(urlInfo: playData, playerInfo: info)
|
||||
|
||||
if Settings.danmuMask {
|
||||
if let mask = info?.dm_mask,
|
||||
let video = playData.dash.video.first,
|
||||
let fps = info?.dm_mask?.fps, fps > 0
|
||||
{
|
||||
maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0))
|
||||
} else if Settings.vnMask {
|
||||
maskProvider = VMaskProvider()
|
||||
}
|
||||
setupMask()
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
data = try? await WebRequest.requestDetailVideo(aid: aid)
|
||||
}
|
||||
setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic)
|
||||
} catch let err {
|
||||
if case let .statusFail(code, message) = err as? RequestError {
|
||||
if code == -404 || code == -10403 {
|
||||
// 解锁港澳台番剧处理
|
||||
do {
|
||||
if let ok = try await fetchAreaLimitVideoData(), ok {
|
||||
return
|
||||
}
|
||||
} catch let err {
|
||||
showErrorAlertAndExit(message: "请求失败,\(err)")
|
||||
}
|
||||
}
|
||||
showErrorAlertAndExit(message: "请求失败\(code) \(message),可能需要大会员")
|
||||
} else if info?.is_upower_exclusive == true {
|
||||
showErrorAlertAndExit(message: "请求失败,该视频为充电专属视频 \(err)")
|
||||
} else {
|
||||
showErrorAlertAndExit(message: "请求失败,\(err)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAreaLimitVideoData() async throws -> Bool? {
|
||||
guard Settings.areaLimitUnlock else { return false }
|
||||
guard let epid = playInfo.epid, epid > 0 else { return false }
|
||||
|
||||
let aid = playInfo.aid
|
||||
let cid = playInfo.cid!
|
||||
|
||||
let season = try await WebRequest.requestBangumiSeasonView(epid: epid)
|
||||
let checkTitle = season.title.contains("僅") ? season.title : season.series_title
|
||||
let checkAreaList = parseAreaByTitle(title: checkTitle)
|
||||
guard !checkAreaList.isEmpty else { return false }
|
||||
|
||||
let playData = try await requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, areaList: checkAreaList)
|
||||
guard let playData = playData else { return false }
|
||||
|
||||
let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid)
|
||||
if info?.last_play_cid == cid, let startTime = info?.playTimeInSecond, playData.dash.duration - startTime > 5, Settings.continuePlay {
|
||||
playerStartPos = startTime
|
||||
} else {
|
||||
playerStartPos = 0
|
||||
}
|
||||
|
||||
await playmedia(urlInfo: playData, playerInfo: info)
|
||||
|
||||
if Settings.danmuMask {
|
||||
if let mask = info?.dm_mask,
|
||||
let video = playData.dash.video.first,
|
||||
let fps = info?.dm_mask?.fps, fps > 0
|
||||
{
|
||||
maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0))
|
||||
} else if Settings.vnMask {
|
||||
maskProvider = VMaskProvider()
|
||||
}
|
||||
setupMask()
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
if let epi = season.episodes.first(where: { $0.ep_id == epid }) {
|
||||
setPlayerInfo(title: epi.index + " " + (epi.index_title ?? ""), subTitle: season.up_info.uname, desp: season.evaluate, pic: epi.cover)
|
||||
}
|
||||
} else {
|
||||
setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func requestAreaLimitPcgPlayUrl(epid: Int, cid: Int, areaList: [String]) async throws -> VideoPlayURLInfo? {
|
||||
for area in areaList {
|
||||
do {
|
||||
return try await WebRequest.requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, area: area)
|
||||
} catch let err {
|
||||
if area == areaList.last {
|
||||
throw err
|
||||
} else {
|
||||
print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func parseAreaByTitle(title: String) -> [String] {
|
||||
if title.isMatch(pattern: "[仅|僅].*[东南亚|其他]") {
|
||||
// TODO: 未支持
|
||||
return []
|
||||
}
|
||||
|
||||
var areas: [String] = []
|
||||
if title.isMatch(pattern: "僅.*台") {
|
||||
areas.append("tw")
|
||||
}
|
||||
if title.isMatch(pattern: "僅.*港") {
|
||||
areas.append("hk")
|
||||
}
|
||||
|
||||
if areas.isEmpty {
|
||||
// 标题没有地区限制信息,返回尝试检测的区域
|
||||
return ["tw", "hk"]
|
||||
} else {
|
||||
return areas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Player
|
||||
|
||||
extension VideoPlayerViewController {
|
||||
@MainActor
|
||||
func prepare(toPlay asset: AVURLAsset, withKeys requestedKeys: [AnyHashable]) {
|
||||
for thisKey in requestedKeys {
|
||||
guard let thisKey = thisKey as? String else {
|
||||
continue
|
||||
}
|
||||
var error: NSError?
|
||||
let keyStatus = asset.statusOfValue(forKey: thisKey, error: &error)
|
||||
if keyStatus == .failed {
|
||||
showErrorAlertAndExit(title: error?.localizedDescription ?? "", message: error?.localizedFailureReason ?? "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !asset.isPlayable {
|
||||
showErrorAlertAndExit(message: "URL解析错误")
|
||||
return
|
||||
}
|
||||
|
||||
playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] time in
|
||||
guard let self else { return }
|
||||
if self.danMuView.isHidden { return }
|
||||
let seconds = time.seconds
|
||||
self.danmuProvider.playerTimeChange(time: seconds)
|
||||
|
||||
if let duration = self.data?.View.duration {
|
||||
BiliBiliUpnpDMR.shared.sendProgress(duration: duration, current: Int(seconds))
|
||||
}
|
||||
|
||||
if let clipInfos = self.clipInfos {
|
||||
var matched = false
|
||||
for clip in clipInfos {
|
||||
if seconds > clip.start, seconds < clip.end {
|
||||
let action = {
|
||||
clip.skipped = true
|
||||
self.player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
if !(clip.skipped ?? false), Settings.autoSkip {
|
||||
action()
|
||||
self.skipAction = nil
|
||||
} else if self.skipAction?.accessibilityLabel != clip.a11Tag {
|
||||
self.skipAction = UIAction(title: clip.customText) { _ in
|
||||
action()
|
||||
}
|
||||
self.skipAction?.accessibilityLabel = clip.a11Tag
|
||||
}
|
||||
|
||||
self.contextualActions = [self.skipAction].compactMap { $0 }
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
self.contextualActions = []
|
||||
}
|
||||
}
|
||||
}
|
||||
if let defaultRate = self.player?.defaultRate,
|
||||
let speed = PlaySpeed.blDefaults.first(where: { $0.value == defaultRate })
|
||||
{
|
||||
self.player = player
|
||||
selectSpeed(AVPlaybackSpeed(rate: speed.value, localizedName: speed.name))
|
||||
} else {
|
||||
self.player = player
|
||||
await viewModel.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
//
|
||||
// NewVideoPlayerViewModel.swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by yicheng on 2024/5/23.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
struct PlayerDetailData {
|
||||
let aid: Int
|
||||
let cid: Int
|
||||
let epid: Int? // 港澳台解锁需要
|
||||
let isBangumi: Bool
|
||||
|
||||
var playerStartPos: Int?
|
||||
var detail: VideoDetail?
|
||||
var clips: [VideoPlayURLInfo.ClipInfo]?
|
||||
var playerInfo: PlayerInfo?
|
||||
var videoPlayURLInfo: VideoPlayURLInfo
|
||||
}
|
||||
|
||||
class VideoPlayerViewModel {
|
||||
var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>()
|
||||
var onPluginRemove = PassthroughSubject<CommonPlayerPlugin, Never>()
|
||||
var onExit: (() -> Void)?
|
||||
var nextProvider: VideoNextProvider?
|
||||
|
||||
private var playInfo: PlayInfo
|
||||
private let danmuProvider = VideoDanmuProvider()
|
||||
private var videoDetail: VideoDetail?
|
||||
private var cancellable = Set<AnyCancellable>()
|
||||
private var playPlugin: CommonPlayerPlugin?
|
||||
|
||||
init(playInfo: PlayInfo) {
|
||||
self.playInfo = playInfo
|
||||
}
|
||||
|
||||
func load() async {
|
||||
do {
|
||||
let data = try await loadVideoInfo()
|
||||
let plugin = await generatePlayerPlugin(data)
|
||||
onPluginReady.send(plugin)
|
||||
} catch let err {
|
||||
onPluginReady.send(completion: .failure(err.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVideoInfo() async throws -> PlayerDetailData {
|
||||
try await initPlayInfo()
|
||||
let data = try await fetchVideoData()
|
||||
await danmuProvider.initVideo(cid: data.cid, startPos: data.playerStartPos ?? 0)
|
||||
return data
|
||||
}
|
||||
|
||||
private func initPlayInfo() async throws {
|
||||
if !playInfo.isCidVaild {
|
||||
playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid)
|
||||
}
|
||||
BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0)
|
||||
}
|
||||
|
||||
private func updateVideoDetailIfNeeded() async {
|
||||
if videoDetail == nil {
|
||||
videoDetail = try? await WebRequest.requestDetailVideo(aid: playInfo.aid)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchVideoData() async throws -> PlayerDetailData {
|
||||
assert(playInfo.isCidVaild)
|
||||
let aid = playInfo.aid
|
||||
let cid = playInfo.cid!
|
||||
async let infoReq = try? WebRequest.requestPlayerInfo(aid: aid, cid: cid)
|
||||
async let detailUpdate: () = updateVideoDetailIfNeeded()
|
||||
do {
|
||||
let playData: VideoPlayURLInfo
|
||||
var clipInfos: [VideoPlayURLInfo.ClipInfo]?
|
||||
|
||||
if playInfo.isBangumi {
|
||||
playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid)
|
||||
clipInfos = playData.clip_info_list
|
||||
} else {
|
||||
playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid)
|
||||
}
|
||||
|
||||
let info = await infoReq
|
||||
_ = await detailUpdate
|
||||
|
||||
var detail = PlayerDetailData(aid: playInfo.aid, cid: playInfo.cid!, epid: playInfo.epid, isBangumi: playInfo.isBangumi, detail: videoDetail, clips: clipInfos, playerInfo: info, videoPlayURLInfo: playData)
|
||||
|
||||
if let info, info.last_play_cid == cid, playData.dash.duration - info.playTimeInSecond > 5, Settings.continuePlay {
|
||||
detail.playerStartPos = info.playTimeInSecond
|
||||
}
|
||||
|
||||
return detail
|
||||
|
||||
} catch let err {
|
||||
if case let .statusFail(code, message) = err as? RequestError {
|
||||
if code == -404 || code == -10403 {
|
||||
// 解锁港澳台番剧处理
|
||||
// do {
|
||||
// if let ok = try await fetchAreaLimitVideoData(), ok {
|
||||
// return
|
||||
// }
|
||||
// } catch let err {
|
||||
// }
|
||||
}
|
||||
throw "\(code) \(message),可能需要大会员"
|
||||
} else if await infoReq?.is_upower_exclusive == true {
|
||||
throw "该视频为充电专属视频 \(err)"
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playNext(newPlayInfo: PlayInfo) {
|
||||
playInfo = newPlayInfo
|
||||
if let playPlugin {
|
||||
onPluginRemove.send(playPlugin)
|
||||
}
|
||||
Task {
|
||||
do {
|
||||
let data = try await loadVideoInfo()
|
||||
let player = BVideoPlayPlugin(detailData: data)
|
||||
onPluginReady.send([player])
|
||||
} catch let err {
|
||||
onPluginReady.send(completion: .failure(err.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func generatePlayerPlugin(_ data: PlayerDetailData) async -> [CommonPlayerPlugin] {
|
||||
let player = BVideoPlayPlugin(detailData: data)
|
||||
let danmu = DanmuViewPlugin(provider: danmuProvider)
|
||||
let upnp = BUpnpPlugin(duration: data.detail?.View.duration)
|
||||
let debug = DebugPlugin()
|
||||
let playSpeed = SpeedChangerPlugin()
|
||||
playSpeed.$currentPlaySpeed.sink { [weak danmu] speed in
|
||||
danmu?.danMuView.playingSpeed = speed.value
|
||||
}.store(in: &cancellable)
|
||||
|
||||
let playlist = VideoPlayListPlugin(nextProvider: nextProvider)
|
||||
playlist.onPlayEnd = { [weak self] in
|
||||
self?.onExit?()
|
||||
}
|
||||
playlist.onPlayNextWithInfo = {
|
||||
[weak self] info in
|
||||
guard let self else { return }
|
||||
playNext(newPlayInfo: info)
|
||||
}
|
||||
|
||||
playPlugin = player
|
||||
|
||||
var plugins: [CommonPlayerPlugin] = [player, danmu, playSpeed, upnp, debug, playlist]
|
||||
|
||||
if let clips = data.clips {
|
||||
let clip = BVideoClipsPlugin(clipInfos: clips)
|
||||
plugins.append(clip)
|
||||
}
|
||||
|
||||
if Settings.danmuMask {
|
||||
if let mask = data.playerInfo?.dm_mask,
|
||||
let video = data.videoPlayURLInfo.dash.video.first,
|
||||
mask.fps > 0
|
||||
{
|
||||
let maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0))
|
||||
plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider))
|
||||
} else if Settings.vnMask {
|
||||
let maskProvider = VMaskProvider()
|
||||
plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider))
|
||||
}
|
||||
}
|
||||
|
||||
if let detail = data.detail {
|
||||
let info = BVideoInfoPlugin(title: detail.title, subTitle: detail.ownerName, desp: detail.View.desc, pic: detail.pic, viewPoints: data.playerInfo?.view_points)
|
||||
plugins.append(info)
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// Created by Yam on 2024/6/9.
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
|
||||
class ReplyCell: UICollectionViewCell {
|
||||
class var identifier: String {
|
||||
return String(describing: Self.self)
|
||||
}
|
||||
|
||||
@IBOutlet var avatarImageView: UIImageView!
|
||||
@IBOutlet var userNameLabel: UILabel!
|
||||
@IBOutlet var contenLabel: UILabel!
|
||||
|
||||
func config(replay: Replys.Reply) {
|
||||
avatarImageView.kf.setImage(
|
||||
with: URL(string: replay.member.avatar),
|
||||
options: [
|
||||
.processor(DownsamplingImageProcessor(size: CGSize(width: 80, height: 80))),
|
||||
.processor(RoundCornerImageProcessor(radius: .widthFraction(0.5))),
|
||||
.cacheSerializer(FormatIndicatedCacheSerializer.png),
|
||||
]
|
||||
)
|
||||
userNameLabel.text = replay.member.uname
|
||||
contenLabel.text = replay.content.message
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder.AppleTV.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="appleTV" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="tvOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22613"/>
|
||||
<capability name="TVUIKit controls" minToolsVersion="10.2"/>
|
||||
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="ReplyCell" id="Eh2-Du-lue" customClass="ReplyCell" customModule="BilibiliLive" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<collectionViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Wff-Lz-AFu">
|
||||
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<tvCardView contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="BbK-et-U1E" customClass="BLCardView" customModule="BilibiliLive" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="582" height="360"/>
|
||||
<tvCardContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="NU3-Bt-p2n">
|
||||
<rect key="frame" x="23" y="23" width="536" height="314"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" tag="1" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QAe-nh-BI1">
|
||||
<rect key="frame" x="20" y="20" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="igG-8K-FYQ"/>
|
||||
<constraint firstAttribute="width" constant="50" id="y0q-EX-6LB"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cNI-2h-EqQ">
|
||||
<rect key="frame" x="78" y="27.5" width="71" height="35"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rG1-i7-Lu2">
|
||||
<rect key="frame" x="20" y="70" width="496" height="244"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="cNI-2h-EqQ" firstAttribute="leading" secondItem="QAe-nh-BI1" secondAttribute="trailing" constant="8" symbolic="YES" id="4dP-d4-GdT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="rG1-i7-Lu2" secondAttribute="trailing" constant="20" id="GSC-ii-sg3"/>
|
||||
<constraint firstAttribute="bottom" secondItem="rG1-i7-Lu2" secondAttribute="bottom" id="Y41-Mq-PTg"/>
|
||||
<constraint firstItem="cNI-2h-EqQ" firstAttribute="centerY" secondItem="QAe-nh-BI1" secondAttribute="centerY" id="eDT-iC-jop"/>
|
||||
<constraint firstItem="rG1-i7-Lu2" firstAttribute="leading" secondItem="NU3-Bt-p2n" secondAttribute="leading" constant="20" id="jyZ-sx-GoO"/>
|
||||
<constraint firstItem="QAe-nh-BI1" firstAttribute="top" secondItem="NU3-Bt-p2n" secondAttribute="top" constant="20" id="nxD-2i-sMk"/>
|
||||
<constraint firstItem="rG1-i7-Lu2" firstAttribute="top" secondItem="QAe-nh-BI1" secondAttribute="bottom" id="p2n-wK-8Mf"/>
|
||||
<constraint firstItem="QAe-nh-BI1" firstAttribute="leading" secondItem="NU3-Bt-p2n" secondAttribute="leading" constant="20" id="q3B-Ru-Hbx"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="12"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tvCardContentView>
|
||||
</tvCardView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="BbK-et-U1E" firstAttribute="top" secondItem="Wff-Lz-AFu" secondAttribute="top" id="Jbi-rF-EEN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BbK-et-U1E" secondAttribute="bottom" id="SIX-9P-hvK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="BbK-et-U1E" secondAttribute="trailing" id="bBd-7k-i5K"/>
|
||||
<constraint firstItem="BbK-et-U1E" firstAttribute="leading" secondItem="Wff-Lz-AFu" secondAttribute="leading" id="fIO-WV-pqN"/>
|
||||
</constraints>
|
||||
</collectionViewCellContentView>
|
||||
<size key="customSize" width="582" height="360"/>
|
||||
<connections>
|
||||
<outlet property="avatarImageView" destination="QAe-nh-BI1" id="1rg-6A-61M"/>
|
||||
<outlet property="contenLabel" destination="rG1-i7-Lu2" id="jx0-u9-gWO"/>
|
||||
<outlet property="userNameLabel" destination="cNI-2h-EqQ" id="IuH-sE-r7M"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-146" y="233"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
</document>
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Published+..swift
|
||||
// BilibiliLive
|
||||
//
|
||||
// Created by yicheng on 2024/6/10.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
fileprivate var cancellables = [String: AnyCancellable]()
|
||||
|
||||
public extension Published {
|
||||
init(wrappedValue defaultValue: Value, key: String) {
|
||||
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
|
||||
self.init(initialValue: value)
|
||||
cancellables[key] = projectedValue.sink { val in
|
||||
UserDefaults.standard.set(val, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,9 @@ import UIKit
|
|||
|
||||
class BiliBiliUpnpDMR: NSObject {
|
||||
static let shared = BiliBiliUpnpDMR()
|
||||
|
||||
weak var currentPlugin: BUpnpPlugin?
|
||||
|
||||
private var udp: GCDAsyncUdpSocket!
|
||||
private var httpServer = HttpServer()
|
||||
private var connectedSockets = [GCDAsyncSocket]()
|
||||
|
@ -221,18 +224,18 @@ class BiliBiliUpnpDMR: NSObject {
|
|||
handlePlay(json: JSON(parseJSON: frame.body))
|
||||
session.sendEmpty()
|
||||
case "Pause":
|
||||
(topMost as? CommonPlayerViewController)?.player?.pause()
|
||||
currentPlugin?.pause()
|
||||
session.sendEmpty()
|
||||
case "Resume":
|
||||
(topMost as? CommonPlayerViewController)?.player?.play()
|
||||
currentPlugin?.resume()
|
||||
session.sendEmpty()
|
||||
case "SwitchDanmaku":
|
||||
let json = JSON(parseJSON: frame.body)
|
||||
(topMost as? CommonPlayerViewController)?.danMuView.isHidden = !json["open"].boolValue
|
||||
Defaults.shared.showDanmu = json["open"].boolValue
|
||||
session.sendEmpty()
|
||||
case "Seek":
|
||||
let json = JSON(parseJSON: frame.body)
|
||||
(topMost as? VideoPlayerViewController)?.player?.seek(to: CMTime(seconds: json["seekTs"].doubleValue, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
currentPlugin?.seek(to: json["seekTs"].doubleValue)
|
||||
session.sendEmpty()
|
||||
case "Stop":
|
||||
(topMost as? CommonPlayerViewController)?.dismiss(animated: true)
|
||||
|
|
|
@ -11,7 +11,7 @@ import Foundation
|
|||
import SwiftyJSON
|
||||
import UIKit
|
||||
|
||||
class LivePlayerViewController: NewCommonPlayerViewController {
|
||||
class LivePlayerViewController: CommonPlayerViewController {
|
||||
var room: LiveRoom?
|
||||
|
||||
private var viewModel: LivePlayerViewModel?
|
||||
|
|
|
@ -707,6 +707,7 @@ struct Replys: Codable, Hashable {
|
|||
|
||||
let member: Member
|
||||
let content: Content
|
||||
let replies: [Reply]?
|
||||
}
|
||||
|
||||
let replies: [Reply]?
|
||||
|
|
|
@ -168,7 +168,8 @@ GEM
|
|||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rexml (3.2.9)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
|
@ -181,6 +182,7 @@ GEM
|
|||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
|
@ -210,6 +212,7 @@ GEM
|
|||
|
||||
PLATFORMS
|
||||
arm64-darwin-21
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
|
Loading…
Reference in New Issue