b面初次测试版

This commit is contained in:
Mr.zhou 2024-05-29 13:20:07 +08:00
parent 249ec76e3b
commit 57a5482045
173 changed files with 21979 additions and 2464 deletions

View File

@ -32,6 +32,16 @@
CB102F5C2BFB244500E967D8 /* MPPositive_RecommendMemberCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB102F5B2BFB244500E967D8 /* MPPositive_RecommendMemberCollectionViewCell.swift */; }; CB102F5C2BFB244500E967D8 /* MPPositive_RecommendMemberCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB102F5B2BFB244500E967D8 /* MPPositive_RecommendMemberCollectionViewCell.swift */; };
CB102F5E2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB102F5D2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift */; }; CB102F5E2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB102F5D2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift */; };
CB1C16522BC80BF100B96AB3 /* MPSideA_MediaCenterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1C16512BC80BF100B96AB3 /* MPSideA_MediaCenterManager.swift */; }; CB1C16522BC80BF100B96AB3 /* MPSideA_MediaCenterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1C16512BC80BF100B96AB3 /* MPSideA_MediaCenterManager.swift */; };
CB24168D2C05D09C007877F7 /* MPPositive_LibraryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24168C2C05D09C007877F7 /* MPPositive_LibraryTableViewCell.swift */; };
CB24168F2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24168E2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift */; };
CB2416912C05D36F007877F7 /* MPPositive_DownloadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2416902C05D36F007877F7 /* MPPositive_DownloadViewModel.swift */; };
CB2416932C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2416922C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift */; };
CB2416952C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2416942C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift */; };
CB2416972C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2416962C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift */; };
CB2416992C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2416982C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift */; };
CB24169B2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24169A2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift */; };
CB24169D2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24169C2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift */; };
CB24169F2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24169E2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift */; };
CB5661292BE09D0500CFD014 /* MPPositive_JsonPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB5661282BE09D0500CFD014 /* MPPositive_JsonPlayer.swift */; }; CB5661292BE09D0500CFD014 /* MPPositive_JsonPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB5661282BE09D0500CFD014 /* MPPositive_JsonPlayer.swift */; };
CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */; }; CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */; };
CBB5D31D2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB5D31C2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift */; }; CBB5D31D2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB5D31C2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift */; };
@ -44,6 +54,8 @@
CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */; }; CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */; };
CBB9F9DD2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB9F9DC2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift */; }; CBB9F9DD2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB9F9DC2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift */; };
CBB9F9DF2BEDDCC5008338DE /* MP_PlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */; }; CBB9F9DF2BEDDCC5008338DE /* MP_PlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */; };
CBBA6A222BFF12030047ADF8 /* MP_AVURLAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBA6A212BFF12030047ADF8 /* MP_AVURLAsset.swift */; };
CBBA6A242BFF160C0047ADF8 /* MP_CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBA6A232BFF160C0047ADF8 /* MP_CacheManager.swift */; };
CBBFA9182BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */; }; CBBFA9182BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */; };
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA9192BBA846600057FD5 /* CoreDataDelegete.swift */; }; CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA9192BBA846600057FD5 /* CoreDataDelegete.swift */; };
CBBFA91E2BBA9B5C00057FD5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */; }; CBBFA91E2BBA9B5C00057FD5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */; };
@ -66,7 +78,6 @@
CBC54E642BC4D5D3003B1901 /* Seawater Surging.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E542BC4D5D3003B1901 /* Seawater Surging.mp3 */; }; CBC54E642BC4D5D3003B1901 /* Seawater Surging.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E542BC4D5D3003B1901 /* Seawater Surging.mp3 */; };
CBC54E652BC4D5D3003B1901 /* Summer Insects.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E552BC4D5D3003B1901 /* Summer Insects.mp3 */; }; CBC54E652BC4D5D3003B1901 /* Summer Insects.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E552BC4D5D3003B1901 /* Summer Insects.mp3 */; };
CBC54E672BC4D90F003B1901 /* Resource.plist in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E662BC4D90F003B1901 /* Resource.plist */; }; CBC54E672BC4D90F003B1901 /* Resource.plist in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E662BC4D90F003B1901 /* Resource.plist */; };
CBC687492BC2882B0023ECA6 /* MPTableManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC687482BC2882B0023ECA6 /* MPTableManager.swift */; };
CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC6874A2BC2B0710023ECA6 /* String.swift */; }; CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC6874A2BC2B0710023ECA6 /* String.swift */; };
CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCAFB592BB3C2A000BC6520 /* LayoutConstraint.swift */; }; CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCAFB592BB3C2A000BC6520 /* LayoutConstraint.swift */; };
CBCAFB5D2BB3C52100BC6520 /* HexColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCAFB5C2BB3C52100BC6520 /* HexColor.swift */; }; CBCAFB5D2BB3C52100BC6520 /* HexColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCAFB5C2BB3C52100BC6520 /* HexColor.swift */; };
@ -134,6 +145,7 @@
CBCC234F2BEE57AC004D7A57 /* MPPositive_PresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCC234E2BEE57AC004D7A57 /* MPPositive_PresentationController.swift */; }; CBCC234F2BEE57AC004D7A57 /* MPPositive_PresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCC234E2BEE57AC004D7A57 /* MPPositive_PresentationController.swift */; };
CBCC23512BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCC23502BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift */; }; CBCC23512BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCC23502BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift */; };
CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCC23522BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift */; }; CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCC23522BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift */; };
CBCF94D42BFED7AD0069EE0B /* AVPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCF94D32BFED7AD0069EE0B /* AVPlayerItem.swift */; };
CBD0CC592BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD0CC582BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift */; }; CBD0CC592BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD0CC582BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift */; };
CBD0CC5E2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD0CC5D2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift */; }; CBD0CC5E2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD0CC5D2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift */; };
CBD313532BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD313522BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift */; }; CBD313532BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD313522BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift */; };
@ -158,6 +170,8 @@
CBD958D42BB6942F00666B0D /* MPSideA_VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */; }; CBD958D42BB6942F00666B0D /* MPSideA_VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */; };
CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */; }; CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */; };
CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */; }; CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */; };
CBE10CB52C0629B50068A396 /* MPPositive_SearchTagModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE10CB42C0629B50068A396 /* MPPositive_SearchTagModel.swift */; };
CBE10CB72C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE10CB62C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift */; };
CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */; }; CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */; };
CBE1CB442BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */; }; CBE1CB442BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */; };
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */; }; CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */; };
@ -222,6 +236,16 @@
CB102F5B2BFB244500E967D8 /* MPPositive_RecommendMemberCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_RecommendMemberCollectionViewCell.swift; sourceTree = "<group>"; }; CB102F5B2BFB244500E967D8 /* MPPositive_RecommendMemberCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_RecommendMemberCollectionViewCell.swift; sourceTree = "<group>"; };
CB102F5D2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_RecommendShowTypeView.swift; sourceTree = "<group>"; }; CB102F5D2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_RecommendShowTypeView.swift; sourceTree = "<group>"; };
CB1C16512BC80BF100B96AB3 /* MPSideA_MediaCenterManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSideA_MediaCenterManager.swift; sourceTree = "<group>"; }; CB1C16512BC80BF100B96AB3 /* MPSideA_MediaCenterManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSideA_MediaCenterManager.swift; sourceTree = "<group>"; };
CB24168C2C05D09C007877F7 /* MPPositive_LibraryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_LibraryTableViewCell.swift; sourceTree = "<group>"; };
CB24168E2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_LoadCoreModel.swift; sourceTree = "<group>"; };
CB2416902C05D36F007877F7 /* MPPositive_DownloadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_DownloadViewModel.swift; sourceTree = "<group>"; };
CB2416922C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_CollectionSongViewModel.swift; sourceTree = "<group>"; };
CB2416942C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_CollectionListViewModel.swift; sourceTree = "<group>"; };
CB2416962C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_CollectionArtistViewModel.swift; sourceTree = "<group>"; };
CB2416982C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_LoveArtistsViewController.swift; sourceTree = "<group>"; };
CB24169A2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_LoveArtistTableViewCell.swift; sourceTree = "<group>"; };
CB24169C2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_LoveSongsViewController.swift; sourceTree = "<group>"; };
CB24169E2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_OfflineSongsViewController.swift; sourceTree = "<group>"; };
CB5661282BE09D0500CFD014 /* MPPositive_JsonPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonPlayer.swift; sourceTree = "<group>"; }; CB5661282BE09D0500CFD014 /* MPPositive_JsonPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonPlayer.swift; sourceTree = "<group>"; };
CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_WebWork.swift; sourceTree = "<group>"; }; CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_WebWork.swift; sourceTree = "<group>"; };
CBB5D31C2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MusicItemShowTableViewCell.swift; sourceTree = "<group>"; }; CBB5D31C2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MusicItemShowTableViewCell.swift; sourceTree = "<group>"; };
@ -234,6 +258,8 @@
CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_DownloadItemModel.swift; sourceTree = "<group>"; }; CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_DownloadItemModel.swift; sourceTree = "<group>"; };
CBB9F9DC2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonLyrics.swift; sourceTree = "<group>"; }; CBB9F9DC2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonLyrics.swift; sourceTree = "<group>"; };
CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_PlayerManager.swift; sourceTree = "<group>"; }; CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_PlayerManager.swift; sourceTree = "<group>"; };
CBBA6A212BFF12030047ADF8 /* MP_AVURLAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_AVURLAsset.swift; sourceTree = "<group>"; };
CBBA6A232BFF160C0047ADF8 /* MP_CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_CacheManager.swift; sourceTree = "<group>"; };
CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_CoreDataHandlerManager.swift; sourceTree = "<group>"; }; CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_CoreDataHandlerManager.swift; sourceTree = "<group>"; };
CBBFA9192BBA846600057FD5 /* CoreDataDelegete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataDelegete.swift; sourceTree = "<group>"; }; CBBFA9192BBA846600057FD5 /* CoreDataDelegete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataDelegete.swift; sourceTree = "<group>"; };
CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; }; CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
@ -256,7 +282,6 @@
CBC54E542BC4D5D3003B1901 /* Seawater Surging.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "Seawater Surging.mp3"; sourceTree = "<group>"; }; CBC54E542BC4D5D3003B1901 /* Seawater Surging.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "Seawater Surging.mp3"; sourceTree = "<group>"; };
CBC54E552BC4D5D3003B1901 /* Summer Insects.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "Summer Insects.mp3"; sourceTree = "<group>"; }; CBC54E552BC4D5D3003B1901 /* Summer Insects.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "Summer Insects.mp3"; sourceTree = "<group>"; };
CBC54E662BC4D90F003B1901 /* Resource.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Resource.plist; sourceTree = "<group>"; }; CBC54E662BC4D90F003B1901 /* Resource.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Resource.plist; sourceTree = "<group>"; };
CBC687482BC2882B0023ECA6 /* MPTableManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPTableManager.swift; sourceTree = "<group>"; };
CBC6874A2BC2B0710023ECA6 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; }; CBC6874A2BC2B0710023ECA6 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
CBCAFB592BB3C2A000BC6520 /* LayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutConstraint.swift; sourceTree = "<group>"; }; CBCAFB592BB3C2A000BC6520 /* LayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutConstraint.swift; sourceTree = "<group>"; };
CBCAFB5C2BB3C52100BC6520 /* HexColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColor.swift; sourceTree = "<group>"; }; CBCAFB5C2BB3C52100BC6520 /* HexColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColor.swift; sourceTree = "<group>"; };
@ -324,6 +349,7 @@
CBCC234E2BEE57AC004D7A57 /* MPPositive_PresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PresentationController.swift; sourceTree = "<group>"; }; CBCC234E2BEE57AC004D7A57 /* MPPositive_PresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PresentationController.swift; sourceTree = "<group>"; };
CBCC23502BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerListShowViewController.swift; sourceTree = "<group>"; }; CBCC23502BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerListShowViewController.swift; sourceTree = "<group>"; };
CBCC23522BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerListShowTableViewCell.swift; sourceTree = "<group>"; }; CBCC23522BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerListShowTableViewCell.swift; sourceTree = "<group>"; };
CBCF94D32BFED7AD0069EE0B /* AVPlayerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItem.swift; sourceTree = "<group>"; };
CBD0CC582BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_BrowseLoadViewModel.swift; sourceTree = "<group>"; }; CBD0CC582BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_BrowseLoadViewModel.swift; sourceTree = "<group>"; };
CBD0CC5D2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_BrowseItemViewModel.swift; sourceTree = "<group>"; }; CBD0CC5D2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_BrowseItemViewModel.swift; sourceTree = "<group>"; };
CBD313522BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_HomeShowTableViewCell.swift; sourceTree = "<group>"; }; CBD313522BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_HomeShowTableViewCell.swift; sourceTree = "<group>"; };
@ -348,6 +374,8 @@
CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSideA_VolumeManager.swift; sourceTree = "<group>"; }; CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSideA_VolumeManager.swift; sourceTree = "<group>"; };
CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonNext.swift; sourceTree = "<group>"; }; CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonNext.swift; sourceTree = "<group>"; };
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerLoadViewModel.swift; sourceTree = "<group>"; }; CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerLoadViewModel.swift; sourceTree = "<group>"; };
CBE10CB42C0629B50068A396 /* MPPositive_SearchTagModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchTagModel.swift; sourceTree = "<group>"; };
CBE10CB62C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchTagCollectionViewCell.swift; sourceTree = "<group>"; };
CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemListModel.swift; sourceTree = "<group>"; }; CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemListModel.swift; sourceTree = "<group>"; };
CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreContentViewController.swift; sourceTree = "<group>"; }; CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreContentViewController.swift; sourceTree = "<group>"; };
CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreListContentCollectionViewCell.swift; sourceTree = "<group>"; }; CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreListContentCollectionViewCell.swift; sourceTree = "<group>"; };
@ -729,11 +757,12 @@
CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */, CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */,
CBD6F2152BF48DDD00343A4A /* MPPositive_ArtistHeaderModel.swift */, CBD6F2152BF48DDD00343A4A /* MPPositive_ArtistHeaderModel.swift */,
CBB5D31E2BDF711600CC333D /* MPPositive_SongItemModel.swift */, CBB5D31E2BDF711600CC333D /* MPPositive_SongItemModel.swift */,
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */,
CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */, CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */,
CBB5F1F82BFC35D000CBF73A /* MPPositive_CollectionSongModel.swift */, CBB5F1F82BFC35D000CBF73A /* MPPositive_CollectionSongModel.swift */,
CBB5F1FA2BFC3DB600CBF73A /* MPPositive_CollectionListModel.swift */, CBB5F1FA2BFC3DB600CBF73A /* MPPositive_CollectionListModel.swift */,
CBB5F1FC2BFC40E400CBF73A /* MPPositive_CollectionArtistModel.swift */, CBB5F1FC2BFC40E400CBF73A /* MPPositive_CollectionArtistModel.swift */,
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */, CBE10CB42C0629B50068A396 /* MPPositive_SearchTagModel.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -752,6 +781,10 @@
CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */, CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */,
CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */, CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */,
CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */, CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */,
CB2416902C05D36F007877F7 /* MPPositive_DownloadViewModel.swift */,
CB2416922C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift */,
CB2416942C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift */,
CB2416962C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift */,
); );
path = ListViewModels; path = ListViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -763,6 +796,7 @@
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */, CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */,
CBF456E22BF2086600ABF761 /* MPPositive_SearchResultsLoadViewModel.swift */, CBF456E22BF2086600ABF761 /* MPPositive_SearchResultsLoadViewModel.swift */,
CB102F592BFB002C00E967D8 /* MPPositive_RecommendLoadViewModel.swift */, CB102F592BFB002C00E967D8 /* MPPositive_RecommendLoadViewModel.swift */,
CB24168E2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift */,
); );
path = LoadViewModels; path = LoadViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -846,6 +880,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CB0918A22BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift */, CB0918A22BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift */,
CB2416982C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift */,
CB24169C2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift */,
CB24169E2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift */,
); );
path = "Center个人曲库页"; path = "Center个人曲库页";
sourceTree = "<group>"; sourceTree = "<group>";
@ -862,6 +899,8 @@
CBCB50202BD118BB009760B3 /* Center */ = { CBCB50202BD118BB009760B3 /* Center */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CB24168C2C05D09C007877F7 /* MPPositive_LibraryTableViewCell.swift */,
CB24169A2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift */,
); );
path = Center; path = Center;
sourceTree = "<group>"; sourceTree = "<group>";
@ -869,6 +908,7 @@
CBCB50212BD118BB009760B3 /* Search */ = { CBCB50212BD118BB009760B3 /* Search */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CBE10CB62C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift */,
CBF456EA2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift */, CBF456EA2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift */,
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */, CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */,
CBF456EC2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift */, CBF456EC2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift */,
@ -963,11 +1003,12 @@
CBE2C4CA2BC7BE5D00F283A7 /* MP_NetWorkManager.swift */, CBE2C4CA2BC7BE5D00F283A7 /* MP_NetWorkManager.swift */,
CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */, CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */,
CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */, CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */,
CBBA6A212BFF12030047ADF8 /* MP_AVURLAsset.swift */,
CBBA6A232BFF160C0047ADF8 /* MP_CacheManager.swift */,
CBCB32192BD7578500802900 /* MP_LocationManager.swift */, CBCB32192BD7578500802900 /* MP_LocationManager.swift */,
CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */, CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */,
CBE2C4C62BC783F700F283A7 /* MP_HUD.swift */, CBE2C4C62BC783F700F283A7 /* MP_HUD.swift */,
CBB5F1FE2BFCB40000CBF73A /* MPPositive_Debouncer.swift */, CBB5F1FE2BFCB40000CBF73A /* MPPositive_Debouncer.swift */,
CBC687482BC2882B0023ECA6 /* MPTableManager.swift */,
CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */, CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */,
CB102F532BFAFA7200E967D8 /* MP_CircularProgressView.swift */, CB102F532BFAFA7200E967D8 /* MP_CircularProgressView.swift */,
CB102F542BFAFA7200E967D8 /* MP_DownloadManager.swift */, CB102F542BFAFA7200E967D8 /* MP_DownloadManager.swift */,
@ -985,6 +1026,7 @@
CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */, CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */,
CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */, CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */,
CBE2C4C82BC7B25800F283A7 /* TableView.swift */, CBE2C4C82BC7B25800F283A7 /* TableView.swift */,
CBCF94D32BFED7AD0069EE0B /* AVPlayerItem.swift */,
); );
path = "Extension(扩展)"; path = "Extension(扩展)";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1203,6 +1245,7 @@
CBD6F21E2BF4B61F00343A4A /* MPPositive_ArtistShowTypeView.swift in Sources */, CBD6F21E2BF4B61F00343A4A /* MPPositive_ArtistShowTypeView.swift in Sources */,
CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */, CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */,
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */, CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */,
CBE10CB52C0629B50068A396 /* MPPositive_SearchTagModel.swift in Sources */,
CBF456E32BF2086600ABF761 /* MPPositive_SearchResultsLoadViewModel.swift in Sources */, CBF456E32BF2086600ABF761 /* MPPositive_SearchResultsLoadViewModel.swift in Sources */,
CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */, CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */,
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */, CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */,
@ -1214,7 +1257,9 @@
CBCB50162BD11402009760B3 /* MPSideA_Home_HeadBannerView.swift in Sources */, CBCB50162BD11402009760B3 /* MPSideA_Home_HeadBannerView.swift in Sources */,
CBCC234F2BEE57AC004D7A57 /* MPPositive_PresentationController.swift in Sources */, CBCC234F2BEE57AC004D7A57 /* MPPositive_PresentationController.swift in Sources */,
CB102F5A2BFB002C00E967D8 /* MPPositive_RecommendLoadViewModel.swift in Sources */, CB102F5A2BFB002C00E967D8 /* MPPositive_RecommendLoadViewModel.swift in Sources */,
CB24169B2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift in Sources */,
CBCB500A2BD11402009760B3 /* MPSideA_CustomTabBar.swift in Sources */, CBCB500A2BD11402009760B3 /* MPSideA_CustomTabBar.swift in Sources */,
CB2416912C05D36F007877F7 /* MPPositive_DownloadViewModel.swift in Sources */,
CBCAFB692BB3CAC400BC6520 /* MP_Lunch_ProgressView.swift in Sources */, CBCAFB692BB3CAC400BC6520 /* MP_Lunch_ProgressView.swift in Sources */,
CBD6F2182BF4A29B00343A4A /* MPPositive_ArtistViewModel.swift in Sources */, CBD6F2182BF4A29B00343A4A /* MPPositive_ArtistViewModel.swift in Sources */,
CBCB50102BD11402009760B3 /* MPSideA_SettingTableViewCell.swift in Sources */, CBCB50102BD11402009760B3 /* MPSideA_SettingTableViewCell.swift in Sources */,
@ -1232,11 +1277,13 @@
CBCB4FFA2BD11402009760B3 /* MPSideA_PrivacyViewController.swift in Sources */, CBCB4FFA2BD11402009760B3 /* MPSideA_PrivacyViewController.swift in Sources */,
CBD6F21C2BF4AEE600343A4A /* MPPositive_ArtistShowHeaderView.swift in Sources */, CBD6F21C2BF4AEE600343A4A /* MPPositive_ArtistShowHeaderView.swift in Sources */,
CBCB500E2BD11402009760B3 /* MPSideA_CenterTableViewCell.swift in Sources */, CBCB500E2BD11402009760B3 /* MPSideA_CenterTableViewCell.swift in Sources */,
CBBA6A222BFF12030047ADF8 /* MP_AVURLAsset.swift in Sources */,
CB102F5E2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift in Sources */, CB102F5E2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift in Sources */,
009662312BB14A5A00FCA65F /* ViewController.swift in Sources */, 009662312BB14A5A00FCA65F /* ViewController.swift in Sources */,
CBEB01852BF5DB3400D45006 /* MPPositive_ArtistDescriptionTableViewCell.swift in Sources */, CBEB01852BF5DB3400D45006 /* MPPositive_ArtistDescriptionTableViewCell.swift in Sources */,
CBE2C4C72BC783F700F283A7 /* MP_HUD.swift in Sources */, CBE2C4C72BC783F700F283A7 /* MP_HUD.swift in Sources */,
CBE2C4C92BC7B25800F283A7 /* TableView.swift in Sources */, CBE2C4C92BC7B25800F283A7 /* TableView.swift in Sources */,
CB24168F2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift in Sources */,
CBCB4F9A2BD11089009760B3 /* MP_NavigationController.swift in Sources */, CBCB4F9A2BD11089009760B3 /* MP_NavigationController.swift in Sources */,
CB0918A32BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift in Sources */, CB0918A32BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift in Sources */,
CBEE8E322BEB0FC0007DA798 /* MPPositive_PlayerCoverView.swift in Sources */, CBEE8E322BEB0FC0007DA798 /* MPPositive_PlayerCoverView.swift in Sources */,
@ -1244,15 +1291,19 @@
CBF456E92BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift in Sources */, CBF456E92BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift in Sources */,
CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */, CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */,
CBE1CB4E2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift in Sources */, CBE1CB4E2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift in Sources */,
CB2416972C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift in Sources */,
CBD313572BD63B390015D227 /* MPPositive_HomeListSecondCollectionViewCell.swift in Sources */, CBD313572BD63B390015D227 /* MPPositive_HomeListSecondCollectionViewCell.swift in Sources */,
0096622D2BB14A5A00FCA65F /* AppDelegate.swift in Sources */, 0096622D2BB14A5A00FCA65F /* AppDelegate.swift in Sources */,
CBC32A532BD8D9F300687171 /* MPPositive_BrowseItemModel.swift in Sources */, CBC32A532BD8D9F300687171 /* MPPositive_BrowseItemModel.swift in Sources */,
CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */, CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */,
CB24169F2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift in Sources */,
CB102F552BFAFA7200E967D8 /* MP_CircularProgressView.swift in Sources */, CB102F552BFAFA7200E967D8 /* MP_CircularProgressView.swift in Sources */,
CBCB4FEC2BD11402009760B3 /* MPSideA_AddViewController.swift in Sources */, CBCB4FEC2BD11402009760B3 /* MPSideA_AddViewController.swift in Sources */,
CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */, CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */,
CBCF94D42BFED7AD0069EE0B /* AVPlayerItem.swift in Sources */,
CBCB50172BD11402009760B3 /* MPSideA_Home_RowListsTableViewCell.swift in Sources */, CBCB50172BD11402009760B3 /* MPSideA_Home_RowListsTableViewCell.swift in Sources */,
CBCB4F982BD11054009760B3 /* MP_BaseViewController.swift in Sources */, CBCB4F982BD11054009760B3 /* MP_BaseViewController.swift in Sources */,
CB2416932C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift in Sources */,
CBD0CC5E2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift in Sources */, CBD0CC5E2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift in Sources */,
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */, CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */,
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */, CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */,
@ -1264,10 +1315,12 @@
CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */, CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */,
CBCB4FE92BD11402009760B3 /* MPSideA_LoadDataMusic.swift in Sources */, CBCB4FE92BD11402009760B3 /* MPSideA_LoadDataMusic.swift in Sources */,
CBCB4FEE2BD11402009760B3 /* MPSideA_BaseViewController.swift in Sources */, CBCB4FEE2BD11402009760B3 /* MPSideA_BaseViewController.swift in Sources */,
CB2416992C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift in Sources */,
CBE1CB582BDE550800701D57 /* MPPositive_ListShowViewController.swift in Sources */, CBE1CB582BDE550800701D57 /* MPPositive_ListShowViewController.swift in Sources */,
CBCB500B2BD11402009760B3 /* MPSideA_CustomTabBarItem.swift in Sources */, CBCB500B2BD11402009760B3 /* MPSideA_CustomTabBarItem.swift in Sources */,
CBB5D31D2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift in Sources */, CBB5D31D2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift in Sources */,
CBEB017D2BF5D35700D45006 /* MPPositive_ArtistShowSongTableViewCell.swift in Sources */, CBEB017D2BF5D35700D45006 /* MPPositive_ArtistShowSongTableViewCell.swift in Sources */,
CB2416952C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift in Sources */,
CBCC23512BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift in Sources */, CBCC23512BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift in Sources */,
CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */, CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */,
CBB5F1F92BFC35D000CBF73A /* MPPositive_CollectionSongModel.swift in Sources */, CBB5F1F92BFC35D000CBF73A /* MPPositive_CollectionSongModel.swift in Sources */,
@ -1284,10 +1337,11 @@
CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */, CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */,
CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift in Sources */, CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift in Sources */,
CBB5F1FD2BFC40E400CBF73A /* MPPositive_CollectionArtistModel.swift in Sources */, CBB5F1FD2BFC40E400CBF73A /* MPPositive_CollectionArtistModel.swift in Sources */,
CBC687492BC2882B0023ECA6 /* MPTableManager.swift in Sources */, CB24168D2C05D09C007877F7 /* MPPositive_LibraryTableViewCell.swift in Sources */,
CBD6F2142BF44D8A00343A4A /* MPPositive_JsonArtist.swift in Sources */, CBD6F2142BF44D8A00343A4A /* MPPositive_JsonArtist.swift in Sources */,
CBD313532BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift in Sources */, CBD313532BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift in Sources */,
CBD6F2162BF48DDD00343A4A /* MPPositive_ArtistHeaderModel.swift in Sources */, CBD6F2162BF48DDD00343A4A /* MPPositive_ArtistHeaderModel.swift in Sources */,
CB24169D2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift in Sources */,
CB0918972BD25D8C006D2B39 /* MPPositive_TabBarController.swift in Sources */, CB0918972BD25D8C006D2B39 /* MPPositive_TabBarController.swift in Sources */,
CBCB500C2BD11402009760B3 /* MPSideA_CustomTabBarView.swift in Sources */, CBCB500C2BD11402009760B3 /* MPSideA_CustomTabBarView.swift in Sources */,
CBB9F9DD2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift in Sources */, CBB9F9DD2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift in Sources */,
@ -1303,9 +1357,11 @@
CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */, CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */,
CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */, CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */,
CBCB4FF02BD11402009760B3 /* MPSideA_PresentationController.swift in Sources */, CBCB4FF02BD11402009760B3 /* MPSideA_PresentationController.swift in Sources */,
CBE10CB72C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift in Sources */,
CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */, CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */,
CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */, CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */,
CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */, CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */,
CBBA6A242BFF160C0047ADF8 /* MP_CacheManager.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -1459,7 +1515,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.2; CURRENT_PROJECT_VERSION = 1.2;
DEVELOPMENT_TEAM = 6HWQW9JC74; DEVELOPMENT_TEAM = T93S37G27F;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MusicPlayer/Info.plist; INFOPLIST_FILE = MusicPlayer/Info.plist;
@ -1497,7 +1553,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.2; CURRENT_PROJECT_VERSION = 1.2;
DEVELOPMENT_TEAM = 6HWQW9JC74; DEVELOPMENT_TEAM = T93S37G27F;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MusicPlayer/Info.plist; INFOPLIST_FILE = MusicPlayer/Info.plist;

View File

@ -9,10 +9,13 @@ import UIKit
import CoreData import CoreData
import AVFoundation import AVFoundation
import Alamofire import Alamofire
import Tiercel
@_exported import IQKeyboardManagerSwift @_exported import IQKeyboardManagerSwift
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
//
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// //
UNUserNotificationCenter.current() UNUserNotificationCenter.current()
@ -22,6 +25,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
print("Users are not allowed to be notified of messages.") print("Users are not allowed to be notified of messages.")
} }
} }
//
DownloadManager.shared.cancelAllTasksIfNeeded()
setAudioSupport() setAudioSupport()
MP_NetWorkManager.shared.requestStatusToYouTube() MP_NetWorkManager.shared.requestStatusToYouTube()
IQKeyboardManager.shared.enable = true IQKeyboardManager.shared.enable = true
@ -32,15 +37,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true return true
} }
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
DownloadManager.shared.session.getAllTasks { tasks in if identifier == "com.yourApp.backgroundDownload" {
for task in tasks { DownloadManager.shared.session = SessionManager("com.yourApp.backgroundDownload", configuration: .init())
task.resume() DownloadManager.shared.session.completionHandler = completionHandler
} }
} }
completionHandler()
}
// //
private func setAudioSupport(){ private func setAudioSupport(){

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1597880530@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1597880530@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Gradient-09@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Gradient-09@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1597880746@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1597880746@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1597880536@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1597880536@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1597880534@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1597880534@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -20,5 +20,21 @@
<string>&quot;Musicoo&quot; needs to obtain your location information in order to refine the preview music information provided to you!</string> <string>&quot;Musicoo&quot; needs to obtain your location information in order to refine the preview music information provided to you!</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>&quot;Musicoo&quot; needs to obtain your location information in order to refine the preview music information provided to you!</string> <string>&quot;Musicoo&quot; needs to obtain your location information in order to refine the preview music information provided to you!</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.lux.musicplayer.MusicPlayer</string>
</dict>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>backgroundFetch</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E214" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E224" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="MPPositive_CollectionArtistModel" representedClassName="MPPositive_CollectionArtistModel" syncable="YES"> <entity name="MPPositive_CollectionArtistModel" representedClassName="MPPositive_CollectionArtistModel" syncable="YES">
<attribute name="artistId" optional="YES" attributeType="String"/> <attribute name="artistId" optional="YES" attributeType="String"/>
<attribute name="coverImage" optional="YES" attributeType="URI"/> <attribute name="coverImage" optional="YES" attributeType="URI"/>
@ -10,26 +10,33 @@
<attribute name="browseId" optional="YES" attributeType="String"/> <attribute name="browseId" optional="YES" attributeType="String"/>
<attribute name="coverImage" optional="YES" attributeType="URI"/> <attribute name="coverImage" optional="YES" attributeType="URI"/>
<attribute name="params" optional="YES" attributeType="String"/> <attribute name="params" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/> <attribute name="title" optional="YES" attributeType="String"/>
</entity> </entity>
<entity name="MPPositive_CollectionSongModel" representedClassName="MPPositive_CollectionSongModel" syncable="YES"> <entity name="MPPositive_CollectionSongModel" representedClassName="MPPositive_CollectionSongModel" syncable="YES">
<attribute name="coverImage" optional="YES" attributeType="URI"/> <attribute name="coverImage" optional="YES" attributeType="URI"/>
<attribute name="lyricsID" optional="YES" attributeType="String"/>
<attribute name="relatedID" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/> <attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/> <attribute name="title" optional="YES" attributeType="String"/>
<attribute name="videoId" optional="YES" attributeType="String"/> <attribute name="videoId" optional="YES" attributeType="String"/>
</entity> </entity>
<entity name="MPPositive_DownloadItemModel" representedClassName="MPPositive_DownloadItemModel" syncable="YES"> <entity name="MPPositive_DownloadItemModel" representedClassName="MPPositive_DownloadItemModel" syncable="YES">
<attribute name="coverImage" optional="YES" attributeType="URI"/> <attribute name="coverImage" optional="YES" attributeType="String"/>
<attribute name="lengthText" optional="YES" attributeType="String"/> <attribute name="lengthText" optional="YES" attributeType="String"/>
<attribute name="longBylineText" optional="YES" attributeType="String"/> <attribute name="longBylineText" optional="YES" attributeType="String"/>
<attribute name="lyrics" optional="YES" attributeType="String"/> <attribute name="lyrics" optional="YES" attributeType="String"/>
<attribute name="lyricsID" optional="YES" attributeType="String"/>
<attribute name="relatedID" optional="YES" attributeType="String"/> <attribute name="relatedID" optional="YES" attributeType="String"/>
<attribute name="resourcePath" optional="YES" attributeType="String"/> <attribute name="reviewImage" optional="YES" attributeType="String"/>
<attribute name="reviewImage" optional="YES" attributeType="URI"/>
<attribute name="shortBylineText" optional="YES" attributeType="String"/> <attribute name="shortBylineText" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/> <attribute name="title" optional="YES" attributeType="String"/>
<attribute name="videoId" optional="YES" attributeType="String"/> <attribute name="videoId" optional="YES" attributeType="String"/>
</entity> </entity>
<entity name="MPPositive_SearchTagModel" representedClassName="MPPositive_SearchTagModel" syncable="YES">
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="text" optional="YES" attributeType="String"/>
</entity>
<entity name="MPSideA_MusicModel" representedClassName="MPSideA_MusicModel" syncable="YES"> <entity name="MPSideA_MusicModel" representedClassName="MPSideA_MusicModel" syncable="YES">
<attribute name="album" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="album" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="author" optional="YES" attributeType="String"/> <attribute name="author" optional="YES" attributeType="String"/>

View File

@ -0,0 +1,22 @@
//
// AVPlayerItem.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/23.
//
import AVFoundation
import ObjectiveC
private var playerItemContext: UInt8 = 0
extension AVPlayerItem {
var uniqueID: String? {
get {
return objc_getAssociatedObject(self, &playerItemContext) as? String
}
set {
objc_setAssociatedObject(self, &playerItemContext, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

View File

@ -79,6 +79,10 @@ extension NotificationCenter{
case pup_player_vc case pup_player_vc
/// ///
case positive_player_reload case positive_player_reload
///
case positive_asset_successfully
///
case positive_player_seek
/// ///
case player_type_switch case player_type_switch
/// ///
@ -87,6 +91,10 @@ extension NotificationCenter{
case net_switch_notReachable case net_switch_notReachable
///- ///-
case net_switch_reachable case net_switch_reachable
///bpushj
case positive_nav_push
///bPop
case positive_nav_pop
} }
} }
} }

View File

@ -20,6 +20,24 @@ extension String {
// type // type
return anyClass return anyClass
} }
//MARK: -
///
func textAutoHeight(width:CGFloat, font:UIFont) ->CGFloat{
let string = self as NSString
let origin = NSStringDrawingOptions.usesLineFragmentOrigin
let lead = NSStringDrawingOptions.usesFontLeading
let ssss = NSStringDrawingOptions.usesDeviceMetrics
let rect = string.boundingRect(with:CGSize(width: width, height:0), options: [origin,lead,ssss], attributes: [NSAttributedString.Key.font:font], context:nil)
return rect.height
}
///
func textAutoWidth(height:CGFloat, font:UIFont) ->CGFloat{
let string = self as NSString
let origin = NSStringDrawingOptions.usesLineFragmentOrigin
let lead = NSStringDrawingOptions.usesFontLeading
let rect = string.boundingRect(with:CGSize(width:0, height: height), options: [origin,lead], attributes: [NSAttributedString.Key.font:font], context:nil)
return rect.width
}
} }
extension Range where Bound == String.Index { extension Range where Bound == String.Index {
func toNSRange(in string: String) -> NSRange { func toNSRange(in string: String) -> NSRange {

View File

@ -55,6 +55,22 @@ typealias ActionBlock = () -> Void?
var MPSideA_ModalType:MPSideA_PresentModal = .Timer var MPSideA_ModalType:MPSideA_PresentModal = .Timer
///B ///B
var MPPositive_ModalType:MPPositive_PresentModal = .PlayerList var MPPositive_ModalType:MPPositive_PresentModal = .PlayerList
///
let DocumentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
///
func getDocumentsFileURL(_ videoID: String) -> String? {
// DocumentsURL
let documentsDirectoryURL = DocumentsURL.appendingPathComponent("Downloads")
// videoIdURL
let fileURL = documentsDirectoryURL.appendingPathComponent("\(videoID).mp4")
//
if FileManager.default.fileExists(atPath: fileURL.path) == true {
//
return fileURL.absoluteString
}else {
return nil
}
}
/// ///
func setTimesToMinSeconds(_ time:TimeInterval) -> String { func setTimesToMinSeconds(_ time:TimeInterval) -> String {
// //

View File

@ -1,100 +0,0 @@
//
// MPTableManager.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/4/7.
//
import UIKit
///tableViewCell(index-tableView-VCcell)
typealias TableViewCellConfigureBlock = (_ index:IndexPath, _ tableView:UITableView) -> UITableViewCell
///tableViewCell(index-tableView-VC)
typealias TableViewCellDidSelectBlock = (_ index:IndexPath, _ tableView:UITableView) -> Void
///tableView
class MPTableManager:NSObject {
///
private var sectionAndItems:[[Any]]?
///cellid()
private var cellIdentifiers:[String]?
//
private var configureCellBlock:TableViewCellConfigureBlock!
//
private var didSelectBlock:TableViewCellDidSelectBlock?
/// UITableView
/// - Parameters:
/// - sectionItems:
/// - cellIdentifier: cellIDsectionItems
/// - configureCellBlock: cell
/// - didSelectBlock: cell
init(_ sectionItems: [[Any]]?, cellIdentifiers: [String]?, configureCellBlock: TableViewCellConfigureBlock?, didSelectBlock: TableViewCellDidSelectBlock?) {
super.init()
self.sectionAndItems = sectionItems
self.cellIdentifiers = cellIdentifiers
self.configureCellBlock = configureCellBlock
self.didSelectBlock = didSelectBlock
}
//
private func item(_ indexPath: IndexPath) -> Any? {
if let items = sectionAndItems?[indexPath.section] {
return items[indexPath.row]
}else {
return nil
}
}
//tableViewdelegate,datasourcecell
func handleTableViewDataSourceAndDelegate(_ tableView: UITableView?, rowHeight:CGFloat = 0) {
guard let table = tableView else {
print("TableView is null")
return
}
table.dataSource = self
table.delegate = self
table.rowHeight = rowHeight != 0 ? rowHeight:UITableView.automaticDimension
table.estimatedRowHeight = 200
guard cellIdentifiers != nil else {
return
}
cellIdentifiers!.forEach { item in
//
guard table.dequeueReusableCell(withIdentifier: item) == nil else { return }
//nib
if let _ = Bundle.main.path(forResource: item, ofType: "nib") {
//nib
table.register(.init(nibName: item, bundle: nil), forCellReuseIdentifier: item)
}else {
//cell
table.register(item.classFromString().self, forCellReuseIdentifier: item)
}
}
}
}
//MARK: - tableView
extension MPTableManager:UITableViewDataSource, UITableViewDelegate {
//
func numberOfSections(in tableView: UITableView) -> Int {
guard sectionAndItems != nil else {
return 0
}
return sectionAndItems!.count
}
//
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let items = sectionAndItems?[section] else {
return 0
}
return items.count
}
//cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = configureCellBlock(indexPath, tableView)
return cell
}
//
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard didSelectBlock != nil else {
return
}
didSelectBlock!(indexPath, tableView)
}
}

View File

@ -0,0 +1,501 @@
//
// MP_AVURLAsset.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/23.
//
import UIKit
import AVFoundation
extension URL {
func convertCustomSchemeURL(_ scheme: String) -> URL? {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
components?.scheme = scheme
return components?.url
}
}
///
@objc protocol MP_AVPlayerItemDelegate {
///
@objc func playerItemReadyToPlay(_ playerItem: MP_AVPlayerItem)
///
@objc func playerItem(_ playerItem: MP_AVPlayerItem, progress:Float)
///
@objc func playerItem(_ playerItem: MP_AVPlayerItem, didFinishLoadingData data:Data)
///,
@objc func playerItemPlaybackStalled(_ playerItem: MP_AVPlayerItem)
///
@objc func playerItem(_ playerItem: MP_AVPlayerItem, loadingError error:Error)
}
///
class MP_AVPlayerItem: AVPlayerItem {
///
var resourceLoaderDelegate:MP_ResourceLoaderDelegate!
///
fileprivate var url:URL
///Scheme
fileprivate let initialScheme:String?
///
fileprivate var customFileExtension:String?
///scheme
fileprivate let customScheme = "myapp"
///
fileprivate var title:String = "Random"
///ID
fileprivate var videoId:String = "Random"
///
fileprivate var resourceAsset:MP_AVURLAsset!
///
weak var delegate:MP_AVPlayerItemDelegate?
///
init(url: URL, bitrate:Int64, customFileExtension: String? = "mp4", title:String?, videoId:String?) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let scheme = components.scheme,
var urlWithCustomScheme = url.convertCustomSchemeURL(customScheme) else {
fatalError("不支持没有scheme的URL")
}
self.url = url
self.initialScheme = scheme
//scheme
if let stringPath = customFileExtension {
//
urlWithCustomScheme.deletePathExtension()
//
urlWithCustomScheme.appendPathExtension(stringPath)
//
self.customFileExtension = stringPath
}
if let title = title, let videoId = videoId {
//
self.title = title
self.videoId = videoId
}
resourceLoaderDelegate = .init(bitrate)
resourceAsset = MP_AVURLAsset(url: urlWithCustomScheme, delegate: resourceLoaderDelegate, title: title ?? "")
//
super.init(asset: resourceAsset, automaticallyLoadedAssetKeys: nil)
//playerItem
resourceLoaderDelegate.playItem = self
//kvo
addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil)
//kvo
addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil)
//kvo
addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil)
//kvo
addObserver(self, forKeyPath: "error", options: [.old,.new], context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
//MARK: - KVO
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else {
return
}
//keyPath
switch keyPath {
case "status"://playerItem
if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue {
//
print("当前音乐-\(title) 已经准备好播放")
}
case "loadedTimeRanges"://
guard self.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId else {return}
let timeRanges = self.loadedTimeRanges.map({$0.timeRangeValue})
//Item
if let first = timeRanges.first {
//
let startSeconds = first.start.seconds
//
let durationSeconds = first.duration.seconds
//
let bufferedSeconds = startSeconds + durationSeconds
//
let maxDuration = CMTimeGetSeconds(self.duration)
let progress = bufferedSeconds/maxDuration
//
delegate?.playerItem(self, progress: Float(progress))
}
case "playbackLikelyToKeepUp"://
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
//
delegate?.playerItemReadyToPlay(self)
}else {
//
delegate?.playerItemPlaybackStalled(self)
}
case "error":
if let error = change?[.newKey] as? Error {
print("当前音乐-\(title) 未做好准备播放,失败原因是\(error)")
delegate?.playerItem(self, loadingError: error)
}
default:
break
}
}
//playerItem
@objc func playbackStalledHandler() {
delegate?.playerItemPlaybackStalled(self)
}
deinit {
print("已销毁\(title)")
NotificationCenter.default.removeObserver(self)
removeObserver(self, forKeyPath: "status")
removeObserver(self, forKeyPath: "loadedTimeRanges")
removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
removeObserver(self, forKeyPath: "error")
//
resourceLoaderDelegate.session?.invalidateAndCancel()
//
resourceAsset = nil
}
}
///
class MP_AVURLAsset: AVURLAsset {
init(url URL: URL, delegate:MP_ResourceLoaderDelegate, title:String) {
super.init(url: URL, options: nil)
self.resourceLoader.setDelegate(delegate, queue: .main)
//AssetAssetresourceLoaderdelegate
//
let keys = ["playable"]
self.loadValuesAsynchronously(forKeys: keys) {
[weak self] in
//
for key in keys {
var error: NSError?
let status = self?.statusOfValue(forKey: key, error: &error)
if status == .loaded {
print("开始对\(title)的预加载")
} else {
//
print("无法加载 \(key): \(String(describing: error))")
//
}
}
}
}
}
///
class MP_ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
//
private var iscompleted:Bool?
//
var mimeType: String?
//
var bitrate:Int64
//
var session: URLSession?
//
var mediaData: Data?
//
var mediaDataBlocks: [MediaDataBlock]?
//
var response: URLResponse?
//
var pendingRequests = Set<AVAssetResourceLoadingRequest>()
//
var maxCount:Int64?
//
var progress:Float = 0
weak var playItem: MP_AVPlayerItem?
//
private var lastRequestedEndOffset:Int64?
init(_ bitrate:Int64) {
self.bitrate = bitrate
super.init()
//seek
NotificationCenter.notificationKey.add(observer: self, selector: #selector(seekAction(_ :)), notificationName: .positive_player_seek)
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard let initialUrl = playItem?.url else {
fatalError("internal inconsistency")
}
//session
if session == nil {
//
if let media = MP_CacheManager.shared.data(forKey: playItem?.videoId ?? "") {
print("缓存中存在\(playItem?.title ?? ""),取出中")
//
mediaData = media.data
mediaDataBlocks = media.dataBlocks
//
if media.isComplete == false {
print("\(playItem?.title ?? "")数据不完整,开始网络请求相关数据")
maxCount = media.maxCount
//
continuationDataRequest(with: initialUrl, count: Int64(media.data.count))
iscompleted = false
}else {
iscompleted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else {return}
if MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId == playItem?.videoId {
playItem?.delegate?.playerItem(playItem!, didFinishLoadingData: media.data)
}
}
}
}else {
print("开始网络请求\(playItem?.title ?? "")相关数据")
//
startDataRequest(with: initialUrl)
}
}else {
}
pendingRequests.insert(loadingRequest)
// processPendingRequests()
return true
}
@objc private func seekAction(_ sender:Notification) {
if playItem?.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId {
if let progress = sender.object as? Double {
let requestedOffset = progress * Double(maxCount ?? 0)
//
if requestedOffset > Double(mediaData?.count ?? 0) {
print("用户滚动到了当前缓存范围外")
// seekURL
let initialUrl = playItem?.url
continuationDataRequest(with: initialUrl!, count: Int64(requestedOffset))
}
}
}
}
///
/// - Parameters:
/// - url:
func startDataRequest(with url: URL) {
//
session?.invalidateAndCancel()
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
session?.dataTask(with: url).resume()
}
///
/// - Parameters:
/// - url:
/// - data:
func continuationDataRequest(with url: URL, count:Int64) {
if session == nil {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}else {
//
session?.getAllTasks(completionHandler: { tasks in
//
tasks.forEach({$0.cancel()})
})
}
//
var request = URLRequest(url: url)
//
let bytes = "bytes=\(count)-"
request.addValue(bytes, forHTTPHeaderField: "Range")
request.addValue(url.host ?? "", forHTTPHeaderField: "Host")
request.addValue("video/mp4", forHTTPHeaderField: "Accept")
session?.dataTask(with: request).resume()
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
pendingRequests.remove(loadingRequest)
}
// MARK: URLSession
///
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard response.mimeType?.contains("video") == true else {
print("\(playItem?.title ?? "")网络地址不可用,响应类型是\(response.mimeType ?? "")")
//
MP_NetWorkManager.shared.requestPlayer(playItem?.videoId ?? "", playlistId: "") {[weak self] resourceUrls, coverUrls in
//
guard let self = self else {return}
print("\(playItem?.title ?? "")重新加载一次")
let url = URL(string: resourceUrls.0.first ?? "")!
playItem?.url = url
if mediaData == nil {
startDataRequest(with: url)
}else {
continuationDataRequest(with: url, count: Int64(mediaData!.count))
}
}
return
}
completionHandler(Foundation.URLSession.ResponseDisposition.allow)
if mediaData == nil {
mediaData = Data()
mediaDataBlocks = []
}
self.response = response
processPendingRequests()
}
///
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let response = dataTask.response as? HTTPURLResponse,
let contentRange = response.allHeaderFields["Accept-Ranges"] as? String else {
print("\(playItem?.title ?? "")数据更新失败")
return
}
// Content-Range
let rangeInfo = extractRangeInfo(contentRange)
//使 DataSegment
let dataSegment = MediaDataBlock(offset: (rangeInfo?.start ?? 0), data: data)
mediaData?.append(data)
mediaDataBlocks?.append(dataSegment)
if mediaData != nil {
//
MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: false, maxCount: self.response?.expectedContentLength ?? 0)
}
processPendingRequests()
//
self.lastRequestedEndOffset = Int64(data.count)
}
// Content-Range
private func extractRangeInfo(_ contentRange: String) -> (start: Int64, end: Int64)? {
// "bytes start-end/total"
let pattern = "bytes (\\d+)-(\\d+)/(\\d+|\\*)"
//
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
return nil
}
// Content-Range
let nsRange = NSRange(contentRange.startIndex..<contentRange.endIndex, in: contentRange)
guard let match = regex.firstMatch(in: contentRange, options: [], range: nsRange) else {
return nil
}
//
let startString = (contentRange as NSString).substring(with: match.range(at: 1))
let endString = (contentRange as NSString).substring(with: match.range(at: 2))
let totalString = (contentRange as NSString).substring(with: match.range(at: 3))
guard let start = Int64(startString), let end = Int64(endString), let total = totalString == "*" ? nil : Int64(totalString) else {
return nil
}
return (start: start, end: end)
}
////
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let errorUnwrapped = error as NSError?, errorUnwrapped.domain == NSURLErrorDomain && errorUnwrapped.code == NSURLErrorCancelled {
//
print("\(playItem?.title ?? "")网络请求被释放了")
if mediaData != nil {
//
MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: true, maxCount: response?.expectedContentLength ?? 0)
}
processPendingRequests()
}else if let error = error {
print("\(playItem?.title ?? "")-网络请求失败,原因为:\(error)")
playItem?.delegate?.playerItem(playItem!, loadingError: error)
return
}else {
//
if mediaData != nil {
//
MP_CacheManager.shared.cacheData(mediaData!, dataBlocks: mediaDataBlocks!, forKey: MP_PlayerManager.shared.loadPlayer.currentVideo?.song.videoId ?? "", isComplete: true, maxCount: response?.expectedContentLength ?? 0)
}
print("\(playItem?.title ?? "")资源加载完成")
//
self.lastRequestedEndOffset = response?.expectedContentLength
processPendingRequests()
playItem?.delegate?.playerItem(playItem!, didFinishLoadingData: mediaData!)
}
}
func processPendingRequests() {
//
let requestsFulfilled = Set<AVAssetResourceLoadingRequest>(pendingRequests.compactMap {
self.fillInContentInformationRequest($0.contentInformationRequest)
if self.haveEnoughDataToFulfillRequest($0.dataRequest!) {
$0.finishLoading()
return $0
}
return nil
})
//
_ = requestsFulfilled.map { self.pendingRequests.remove($0) }
}
///contentInformationRequest
func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
//
if iscompleted == true {
contentInformationRequest?.contentType = "video/mp4"
self.maxCount = Int64(mediaData!.count)
contentInformationRequest?.contentLength = Int64(mediaData!.count)
contentInformationRequest?.isByteRangeAccessSupported = true
return
}
//contentInformationRequest
if let responseUnwrapped = response {
contentInformationRequest?.contentType = responseUnwrapped.mimeType
self.maxCount = responseUnwrapped.expectedContentLength
contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength
contentInformationRequest?.isByteRangeAccessSupported = true
}else if let maxCount = maxCount {
contentInformationRequest?.contentType = "video/mp4"
contentInformationRequest?.contentLength = maxCount
contentInformationRequest?.isByteRangeAccessSupported = true
}
}
///
func haveEnoughDataToFulfillRequest(_ dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
let requestedOffset = Int64(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
let currentOffset = Int64(dataRequest.currentOffset)
// Data
var responseData = Data()
//
//
for item in (mediaDataBlocks ?? []).sorted(by: { $0.offset < $1.offset }) {
//
if item.offset < currentOffset + Int64(requestedLength) && item.offset + Int64(item.data.count) > currentOffset {
// responseData
let start = max(currentOffset, item.offset) - item.offset
let end = min(currentOffset + Int64(requestedLength), item.offset + Int64(item.data.count)) - item.offset
if start < end { //
responseData.append(item.data.subdata(in: Int(start)..<Int(end)))
}
}
}
// responseData
if responseData.count >= requestedLength {
// 使dataRequest
dataRequest.respond(with: responseData)
return true
}
// false
return false
// guard let songDataUnwrapped = mediaData,
// songDataUnwrapped.count > currentOffset else {
// return false
// }
// let bytesToRespond = min(songDataUnwrapped.count - currentOffset, requestedLength)
// let dataToRespond = songDataUnwrapped.subdata(in: Range(uncheckedBounds: (currentOffset, currentOffset + bytesToRespond)))
// dataRequest.respond(with: dataToRespond)
// return songDataUnwrapped.count >= requestedLength + requestedOffset
}
deinit {
NotificationCenter.default.removeObserver(self)
session?.invalidateAndCancel()
mediaData = nil
mediaDataBlocks = nil
}
}
///
struct MediaDataBlock: Codable {
///
var offset:Int64
///
var data:Data
}

View File

@ -0,0 +1,143 @@
//
// MP_CacheManager.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/23.
//
import Foundation
///
class CachedMedia: NSObject, Codable {
//
let data: Data
//
let dataBlocks:[MediaDataBlock]
//
let isComplete: Bool
//
let maxCount:Int64
init(data: Data, dataBlocks:[MediaDataBlock], isComplete: Bool, maxCount:Int64) {
self.data = data
self.dataBlocks = dataBlocks
self.isComplete = isComplete
self.maxCount = maxCount
}
}
///
class MP_CacheManager {
// 访
static let shared = MP_CacheManager()
// 1videoID2
private let memoryCache = NSCache<NSString, CachedMedia>()
//
private let fileManager = FileManager.default
//
private let cacheDirectory: URL
//线
private var cacheQueue = DispatchQueue(label: "com.MP_CacheManager.cacheQueue")
//
private var cacheOperations: [String: DispatchWorkItem] = [:]
//
private let throttleInterval: TimeInterval = 1.0
//
private let expirationInterval: TimeInterval = 86400 // 24
private init() {
//
let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)
//
cacheDirectory = urls[0].appendingPathComponent("MyAssetCacheManager")
//
if !fileManager.fileExists(atPath: cacheDirectory.path) {
//
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil)
}
}
///
/// - Parameters:
/// - data:
/// - key: 使videoId
func cacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) {
// cacheQueue 线
cacheQueue.async { [weak self] in
// key
self?.cacheOperations[key]?.cancel()
//
let operation = DispatchWorkItem {
self?.performCacheData(data, dataBlocks: dataBlocks, forKey: key, isComplete: isComplete, maxCount: maxCount)
// cacheQueue
self?.cacheQueue.async {
self?.cacheOperations.removeValue(forKey: key)
}
}
// operation
self?.cacheOperations[key] = operation
// operation
self?.cacheQueue.asyncAfter(deadline: .now() + self!.throttleInterval, execute: operation)
}
}
//
private func performCacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) {
let contentToCache = CachedMedia(data: data, dataBlocks: dataBlocks, isComplete: isComplete, maxCount: maxCount)
memoryCache.setObject(contentToCache, forKey: key as NSString)
//
let fileURL = self.cacheDirectory.appendingPathComponent(key)
//
let tempURL = fileURL.appendingPathExtension("tmp")
let encoder = JSONEncoder()
do {
let newData = try encoder.encode(contentToCache)
//
try newData.write(to: tempURL, options: .atomicWrite)
//
if self.fileManager.fileExists(atPath: fileURL.path) {
try self.fileManager.removeItem(at: fileURL)
}
//
try self.fileManager.moveItem(at: tempURL, to: fileURL)
//
let expirationDate = Date().addingTimeInterval(self.expirationInterval)
try self.fileManager.setAttributes([.modificationDate: expirationDate], ofItemAtPath: fileURL.path)
} catch {
//
try? self.fileManager.removeItem(at: tempURL)
print("无法将密钥数据写入磁盘 \(key) - 错误: \(error)")
}
}
///
/// - Parameter key: 使videoId
/// - Returns:
func data(forKey key: String) -> CachedMedia? {
if let cachedContent = memoryCache.object(forKey: key as NSString){
return cachedContent
}
return cacheQueue.sync {
[weak self] in
guard let self = self else { return nil }
let fileURL = self.cacheDirectory.appendingPathComponent(key)
guard let jsonData = try? Data(contentsOf: fileURL) else { return nil }
// JSON CachedMedia
let decoder = JSONDecoder()
do{
let cachedContent = try decoder.decode(CachedMedia.self, from: jsonData)
//
let attributes = try fileManager.attributesOfItem(atPath: fileURL.path)
guard let modificationDate = attributes[.modificationDate] as? Date else { return nil }
if Date().timeIntervalSince(modificationDate) < expirationInterval{
//
memoryCache.setObject(cachedContent, forKey: key as NSString)
return cachedContent
} else {
//
try fileManager.removeItem(at: fileURL)
}
} catch {
print("读取缓存数据失败 \(key) - 错误: \(error)")
}
return nil
}
}
}

View File

@ -7,78 +7,85 @@
import Foundation import Foundation
import Foundation import Foundation
import Alamofire import Tiercel
class DownloadManager: NSObject {
class DownloadManager: NSObject, URLSessionDownloadDelegate {
static let shared = DownloadManager() static let shared = DownloadManager()
var session: URLSession! var session: SessionManager!
var progressHandlers: [URL: (CGFloat) -> Void] = [:] var progressHandlers: [URL: (CGFloat) -> Void] = [:]
var completionHandlers: [URL: (Result<URL, Error>) -> Void] = [:] var completionHandlers: [URL: (Result<MPPositive_SongItemModel, Error>) -> Void] = [:]
var downloadTasks: [URL: URLSessionDownloadTask] = [:] var downloadTasks: [URL: URLSessionDownloadTask] = [:]
var progressStorage: [URL: CGFloat] = [:] // var progressStorage: [URL: CGFloat] = [:] //
var songHandlers:[URL: MPPositive_SongItemModel] = [:]
private override init() { private override init() {
super.init() super.init()
let configuration = URLSessionConfiguration.background(withIdentifier: "com.yourApp.backgroundDownload") var configuration = SessionConfiguration()
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) configuration.timeoutIntervalForRequest = 60
configuration.maxConcurrentTasksLimit = 6
configuration.allowsCellularAccess = true
session = SessionManager("com.yourApp.backgroundDownload", configuration: configuration)
} }
func downloadVideo(from url: URL, song:MPPositive_SongItemModel, progressHandler: @escaping (CGFloat) -> Void, completion: @escaping (Result<MPPositive_SongItemModel, Error>) -> Void) {
func downloadVideo(from url: URL, videoId: String, progressHandler: @escaping (CGFloat) -> Void, completion: @escaping (Result<URL, Error>) -> Void) {
let downloadTask = session.downloadTask(with: url)
progressHandlers[url] = progressHandler progressHandlers[url] = progressHandler
completionHandlers[url] = completion completionHandlers[url] = completion
downloadTasks[url] = downloadTask songHandlers[url] = song
downloadTask.resume() let downloadTask = session.download(url, headers: ["Accept-Encoding": "gzip, deflate"])
} //
downloadTask?.progress(handler: { [weak self] (task) in
guard let self = self else {return}
progressHandlers[task.url]?(task.progress.fractionCompleted)
progressStorage[task.url] = task.progress.fractionCompleted
})
//
downloadTask?.success(handler: { [weak self] (task) in
guard let self = self else {return}
//,
let originalURL = task.url
//
let filePathUrl:URL = URL(fileURLWithPath: task.filePath)
print("任务下载地址:\(filePathUrl)")
let downloadsURL = DocumentsURL.appendingPathComponent("Downloads")
if !FileManager.default.fileExists(atPath: downloadsURL.path) {
do {
try FileManager.default.createDirectory(at: downloadsURL, withIntermediateDirectories: true, attributes: nil)
} catch {
completionHandlers[originalURL]?(.failure(error))
return
}
}
let fileURL = downloadsURL.appendingPathComponent("\(MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId ?? "").mp4")
do {
try FileManager.default.moveItem(at: filePathUrl, to: fileURL)
//VideoID
completionHandlers[originalURL]?(.success(songHandlers[originalURL]!))
progressStorage[originalURL] = nil //
} catch {
completionHandlers[originalURL]?(.failure(error))
}
}).failure(handler: { [weak self] (task) in
guard let self = self else {return}
//
let originalURL = task.url
if let error = task.error {
completionHandlers[originalURL]?(.failure(error))
}
})
}
func getProgress(for url: URL) -> CGFloat? { func getProgress(for url: URL) -> CGFloat? {
return progressStorage[url] return progressStorage[url]
} }
func cancelAllTasksIfNeeded() {
// URLSessionDownloadDelegate methods //
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { for key in progressStorage.keys {
guard let originalURL = downloadTask.originalRequest?.url else { return } session.cancel(key)
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let downloadsURL = documentsURL.appendingPathComponent("Downloads")
if !FileManager.default.fileExists(atPath: downloadsURL.path) {
do {
try FileManager.default.createDirectory(at: downloadsURL, withIntermediateDirectories: true, attributes: nil)
} catch {
completionHandlers[originalURL]?(.failure(error))
return
}
}
let fileURL = downloadsURL.appendingPathComponent("\(MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId ?? "").mp4")
do {
try FileManager.default.moveItem(at: location, to: fileURL)
completionHandlers[originalURL]?(.success(fileURL))
progressStorage[originalURL] = nil //
} catch {
completionHandlers[originalURL]?(.failure(error))
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
guard let originalURL = downloadTask.originalRequest?.url else { return }
let progress = CGFloat(totalBytesWritten) / CGFloat(totalBytesExpectedToWrite)
progressHandlers[originalURL]?(progress)
progressStorage[originalURL] = progress //
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let originalURL = task.originalRequest?.url else { return }
if let error = error {
completionHandlers[originalURL]?(.failure(error))
} }
} }
} }
//class DownloadManager { //class DownloadManager {
// //
// static let shared = DownloadManager() // static let shared = DownloadManager()

View File

@ -64,6 +64,8 @@ class MP_NetWorkManager: NSObject {
//MARK: - //MARK: -
//访 //访
private var visitorData:String? private var visitorData:String?
//
private lazy var currTimeDate:String = (Date().timeZone() - 7.days).toString(.custom("YYYYMMdd"))
/// ///
private lazy var locaton:String = MP_LocationManager.shared.requestLocation() private lazy var locaton:String = MP_LocationManager.shared.requestLocation()
// //
@ -88,7 +90,7 @@ class MP_NetWorkManager: NSObject {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -105,9 +107,11 @@ class MP_NetWorkManager: NSObject {
} }
} }
} }
//MARK: - //MARK: - GCD
//- ///-
private var browseQueque:DispatchQueue? private var browseQueque:DispatchQueue?
///-
var playerItemLoadingGroup:DispatchGroup = DispatchGroup()
//MARK: - //MARK: -
/// ///
var browseRequestStateBlock:BrowseRequestStateBlock? var browseRequestStateBlock:BrowseRequestStateBlock?
@ -200,7 +204,7 @@ extension MP_NetWorkManager {
//web //web
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -209,7 +213,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostHomeBrowse(url, parameters: parameters) requestPostHomeBrowse(url, parameters: parameters)
} }
@ -263,7 +267,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -272,7 +276,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} // //guard netWorkStatu != .notReachable else {return}
requestPostAlbumOrList(url, parameters: parameters) { results in requestPostAlbumOrList(url, parameters: parameters) { results in
comletion(results) comletion(results)
} }
@ -320,7 +324,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -329,7 +333,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostArtist(url, parameters: parameters) { result in requestPostArtist(url, parameters: parameters) { result in
comletion(result) comletion(result)
} }
@ -376,7 +380,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -385,7 +389,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostArtistMore(url, parameters: parameters) { result in requestPostArtistMore(url, parameters: parameters) { result in
comletion(result) comletion(result)
} }
@ -429,7 +433,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -438,7 +442,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostArtistMoreContinuation(url, parameters: parameters) { result in requestPostArtistMoreContinuation(url, parameters: parameters) { result in
comletion(result) comletion(result)
} }
@ -483,7 +487,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -492,7 +496,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostNextList(url, parameters: parameters) { listSongs in requestPostNextList(url, parameters: parameters) { listSongs in
// //
completion(listSongs) completion(listSongs)
@ -534,7 +538,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -543,7 +547,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostNextLyricsAndRelated(url, parameters: parameters) { result in requestPostNextLyricsAndRelated(url, parameters: parameters) { result in
completion(result) completion(result)
} }
@ -568,7 +572,7 @@ extension MP_NetWorkManager {
//MARK: - player //MARK: - player
/// Player(/) /// Player(/)
/// - Parameter item: /// - Parameter item:
func requestPlayer(_ item: MPPositive_SongItemModel, completion:@escaping ((([String],[String]), [String]?) -> Void)){ func requestPlayer(_ videoId: String, playlistId: String, completion:@escaping ((([String],[Float],[String]), [String]?) -> Void)){
//player //player
let path = header+point+player let path = header+point+player
//url //url
@ -578,13 +582,27 @@ extension MP_NetWorkManager {
} }
//videoIdparams //videoIdparams
let parameters:[String:Any] = [ let parameters:[String:Any] = [
"videoId":(item.videoId ?? ""), "videoId":videoId,
"prettyPrint":"false", "prettyPrint":"false",
// "playlistId":"OLAK5uy_knZiqQOlTDeQ3jecXrW_VIAZKdMnkLGgw",
"context":[ "context":[
"client":[ "client":[
//访 // //访
// "clientName": "WEB_REMIX",
// "clientVersion": "1.\(currTimeDate).01.00"
//web
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00" "visitorData":visitorData,
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36,gzip(gfe)",
// "remoteHost":"2401:b60:13:4335:bea3:7c0b:5c4e:db84",
"originalUrl":"https://music.youtube.com/watch?v=\(videoId)",
//访
"clientVersion": "1.\(currTimeDate).01.00",
"platform":"MOBILE",
//
"hl":Language_first_local,
//
"gl":locaton
] ]
], ],
"playbackContext": [ "playbackContext": [
@ -593,13 +611,13 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in requestPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in
completion(resourceUlrs, coverUrls) completion(resourceUlrs, coverUrls)
} }
} }
/// ///
private func requestPostPlayer(_ url:URL, parameters:Parameters, completion:@escaping((([String],[String]), [String]?) -> Void)) { private func requestPostPlayer(_ url:URL, parameters:Parameters, completion:@escaping((([String],[Float],[String]), [String]?) -> Void)) {
//post //post
MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonPlayer.self) { [weak self] (response) in MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonPlayer.self) { [weak self] (response) in
guard let self = self else {return} guard let self = self else {return}
@ -636,7 +654,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -645,7 +663,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostLyric(url, parameters: parameters) { lyrics in requestPostLyric(url, parameters: parameters) { lyrics in
completion(lyrics) completion(lyrics)
} }
@ -683,7 +701,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -692,7 +710,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostRecommend(url, parameters: parameters) { results in requestPostRecommend(url, parameters: parameters) { results in
completion(results) completion(results)
} }
@ -736,7 +754,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -745,7 +763,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostSearchSuggestions(url, parameters: parameters) { result in requestPostSearchSuggestions(url, parameters: parameters) { result in
completion(result) completion(result)
} }
@ -787,7 +805,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -796,7 +814,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostSearchPreviewResults(url, parameters: parameters) { result in requestPostSearchPreviewResults(url, parameters: parameters) { result in
completion(result) completion(result)
} }
@ -844,7 +862,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -853,7 +871,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostSearchTypeResults(url, parameters: parameters) { result in requestPostSearchTypeResults(url, parameters: parameters) { result in
completion(result) completion(result)
} }
@ -901,7 +919,7 @@ extension MP_NetWorkManager {
"clientName": "WEB_REMIX", "clientName": "WEB_REMIX",
"visitorData":visitorData, "visitorData":visitorData,
//访 //访
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "clientVersion": "1.\(currTimeDate).01.00",
"platform":"DESKTOP", "platform":"DESKTOP",
// //
"hl":Language_first_local, "hl":Language_first_local,
@ -910,7 +928,7 @@ extension MP_NetWorkManager {
] ]
] ]
] ]
guard netWorkStatu != .notReachable else {return} //guard netWorkStatu != .notReachable else {return}
requestPostSearchTypeContinuation(url, parameters: parameters) { result in requestPostSearchTypeContinuation(url, parameters: parameters) { result in
completion(result) completion(result)
} }
@ -1203,15 +1221,15 @@ extension MP_NetWorkManager {
/// - Parameters: /// - Parameters:
/// - player: player /// - player: player
/// - completion: /// - completion:
private func parsingPlayer(_ player:JsonPlayer, completion:@escaping((([String],[String]), [String]?) -> Void)){ private func parsingPlayer(_ player:JsonPlayer, completion:@escaping((([String],[Float],[String]), [String]?) -> Void)){
var infos:[String]? var infos:[String]?
//player //player
if let videoDetails = player.videoDetails { if let videoDetails = player.videoDetails {
infos = parsingPlayerVideoDetails(videoDetails) infos = parsingPlayerVideoDetails(videoDetails)
} }
if let streamingData = player.streamingData { if let streamingData = player.streamingData {
parsingPlayerStreamingData(streamingData){ audios,videos in parsingPlayerStreamingData(streamingData){ videos,floats,approxDurationMs in
completion((audios,videos),infos) completion((videos,floats,approxDurationMs),infos)
} }
} }
} }
@ -1221,10 +1239,11 @@ extension MP_NetWorkManager {
/// - Parameters: /// - Parameters:
/// - streamingData: /// - streamingData:
/// - completion: /// - completion:
private func parsingPlayerStreamingData(_ streamingData:JsonPlayer.StreamingData, completion:@escaping(([String],[String]) -> Void)) { private func parsingPlayerStreamingData(_ streamingData:JsonPlayer.StreamingData, completion:@escaping(([String],[Float],[String]) -> Void)) {
var group:DispatchGroup? = DispatchGroup() var group:DispatchGroup? = DispatchGroup()
var videos:[String] = [] var videos:[String] = []
var audios:[String] = [] var floats:[Float] = []
var approxDurationMs:[String] = []
let allFormats = (streamingData.formats ?? []) + (streamingData.adaptiveFormats ?? []) let allFormats = (streamingData.formats ?? []) + (streamingData.adaptiveFormats ?? [])
for format in allFormats { for format in allFormats {
if let signatureCipher = format.signatureCipher { if let signatureCipher = format.signatureCipher {
@ -1232,21 +1251,17 @@ extension MP_NetWorkManager {
group?.enter() group?.enter()
// //
parsingPlayerSignatureCipher(signatureCipher) { result in parsingPlayerSignatureCipher(signatureCipher) { result in
// //
if format.mimeType?.contains("video") == true { videos.append(result)
// floats.append(format.bitrate ?? 0)
videos.append(result) approxDurationMs.append(format.approxDurationMs ?? "")
}
if format.mimeType?.contains("audio") == true {
audios.append(result)
}
// DispatchGroup // DispatchGroup
group?.leave() group?.leave()
} }
} }
} }
group?.notify(queue: .main) { group?.notify(queue: .main) {
completion(audios,videos) completion(videos, floats, approxDurationMs)
group = nil group = nil
} }
} }
@ -1270,11 +1285,11 @@ extension MP_NetWorkManager {
//URl //URl
let urlSubstring = originalURLString[urlStartIndex...] // &url= let urlSubstring = originalURLString[urlStartIndex...] // &url=
let signString = String(originalURLString[sRange.upperBound..<spSigRange.lowerBound]) let signString = String(originalURLString[sRange.upperBound..<spSigRange.lowerBound])
// //
MP_WebWork.shared.excuteJavaScript(signString) { result in MP_WebWork.shared.excuteJavaScript(signString) { result in
// //
let abString = urlSubstring + "&sig=" + result let abString = urlSubstring + "&sig=" + result
// print("Resources-SignatureDecryption:\(abString)")
completion(abString) completion(abString)
} }
} }

View File

@ -9,6 +9,7 @@ import UIKit
import AVFoundation import AVFoundation
import MediaPlayer import MediaPlayer
import AVKit import AVKit
import FreeStreamer
/// ///
enum MP_PlayerStateType:Int { enum MP_PlayerStateType:Int {
/// ///
@ -43,13 +44,20 @@ typealias MP_PlayTimerStopAction = () -> Void
/// ///
typealias MP_PlayTimerEditEndAction = () -> Void typealias MP_PlayTimerEditEndAction = () -> Void
/// ///
typealias MP_PlayCacheValueAction = (_ currentValue:TimeInterval, _ duration:TimeInterval) -> Void typealias MP_PlayCacheValueAction = (Float) -> Void
/// ///
class MP_PlayerManager:NSObject{ class MP_PlayerManager:NSObject{
/// ///
static let shared = MP_PlayerManager() static let shared = MP_PlayerManager()
/// ///
private var player:AVPlayer = AVPlayer() // private var player:AVPlayer = AVPlayer()
///
private var player:FSAudioStream!
// ///
private var next:FSAudioStream!
///
private var timer:DispatchSourceTimer!
///load ///load
var loadPlayer:MPPositive_PlayerLoadViewModel!{ var loadPlayer:MPPositive_PlayerLoadViewModel!{
didSet{ didSet{
@ -68,6 +76,7 @@ class MP_PlayerManager:NSObject{
didSet{ didSet{
// //
NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState) NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState)
} }
} }
/// ///
@ -95,22 +104,73 @@ class MP_PlayerManager:NSObject{
} }
/// ///
private var startActionBlock:MP_PlayTimerStartAction! private var startActionBlock:MP_PlayTimerStartAction!
/// ///
var runActionBlock:MP_PlayTimerRunAction! var runActionBlock:MP_PlayTimerRunAction!
/// ///
var cacheValueBlock:MP_PlayCacheValueAction! var cacheValueBlock:MP_PlayCacheValueAction!
private override init() { private override init() {
super.init() super.init()
// // player.automaticallyWaitsToMinimizeStalling = false
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) //// player.delegate = self
// //
// NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
// //
NotificationCenter.notificationKey.add(observer: self, selector: #selector(userSwitchCurrentVideoAction(_ :)), notificationName: .positive_player_reload) NotificationCenter.notificationKey.add(observer: self, selector: #selector(userSwitchCurrentVideoAction(_ :)), notificationName: .positive_player_reload)
// //
NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkReachableAction(_ :)), notificationName: .net_switch_reachable) // NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkReachableAction(_ :)), notificationName: .net_switch_reachable)
//
let queue = DispatchQueue(label: "com.MPPlayerTimer.queue")
//
timer = DispatchSource.makeTimerSource(queue: queue)
//
timer!.schedule(deadline: .now(), repeating: .seconds(1))
//
timer.setEventHandler {
[weak self] in
guard let self = self else {return}
timerAction()
}
//
timer.resume()
} }
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
player = nil
timer.cancel()
timer = nil
} }
///
private func timerAction() {
//
guard let player = player, playState == .Playing else {return}
DispatchQueue.main.async {[weak self] in
guard let self = self, findTurePlayer(player) else {return}
//
let duration = player.duration.playbackTimeInSeconds
let currentTime = player.currentTimePlayed.playbackTimeInSeconds
//
if self.runActionBlock != nil {
self.runActionBlock!(TimeInterval(currentTime), TimeInterval(duration))
}
//
if player.cached == true || loadPlayer.currentVideo.isDlownd == true {
if self.cacheValueBlock != nil {
self.cacheValueBlock!(1)
}
}else {
//
let byte = currentTime + 100
let rate = Float(byte)/duration
if self.cacheValueBlock != nil {
self.cacheValueBlock!(rate)
}
}
}
}
/// ///
/// - Parameters: /// - Parameters:
/// - startAction: /// - startAction:
@ -122,143 +182,102 @@ class MP_PlayerManager:NSObject{
print("Player No Data") print("Player No Data")
return return
} }
// //
switch playState { stopAndReleaseStream(&player)
case .Null://
break
case .Playing://
player.pause()
case .Pause://
break
}
// //
if startAction != nil { if startAction != nil {
startActionBlock = startAction startActionBlock = startAction
} }
//playerItem if next != nil, (next.url == (loadPlayer.currentVideo.resourcePlayerURL! as NSURL)) {
player.replaceCurrentItem(with: loadPlayer.currentVideo.resourcePlayerItem) player = next
//0 }else {
player.seek(to: .zero) //
// player = .init(url: loadPlayer.currentVideo.resourcePlayerURL!)
let interval:CMTime = .init(seconds: 1, preferredTimescale: .init(1)) player.maxRetryCount = 3
//线
player.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] (time) in
guard let self = self else { return }
//
let currentDuration = CMTimeGetSeconds(time)
//
let maxDuration = getMusicDuration()
if maxDuration.isNaN == false {
//
if currentDuration <= maxDuration {
//
if runActionBlock != nil {
runActionBlock!(currentDuration, maxDuration)
}
}
}
})
//PlayerItem
//
loadPlayer.currentVideo?.resourcePlayerItem?.addObserver(self, forKeyPath: "status", options: [.old,.new], context: nil)
//
loadPlayer.currentVideo?.resourcePlayerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.old,.new], context: nil)
//
loadPlayer.currentVideo?.resourcePlayerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old,.new], context: nil)
//
loadPlayer.listViewVideos.forEach({$0.preloadAsset($0.resourceAsset)})
}
///
@objc private func netWorkReachableAction(_ sender:Notification) {
//
if loadPlayer?.currentVideo != nil {
//
let currentTime = loadPlayer.currentVideo!.resourcePlayerItem.currentTime()
//
player.seek(to: currentTime)
player.play()
playState = .Playing
} }
} //
let index = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo) ?? 0
//KVO if (loadPlayer.listViewVideos.count-1) > index {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { stopAndReleaseStream(&next)
guard let keyPath = keyPath else { //URL
return let nextURL = loadPlayer.listViewVideos[index + 1].resourcePlayerURL
next = preloadNext(nextURL!)
} }
//keyPath //
switch keyPath { player.play()
case "status"://playerItem //
if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue { player.onStateChange = {
// [weak self] status in
if playState != .Playing { guard let self = self, findTurePlayer(player) else {return}
//statuVlaueplayerItem switch status {
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 已经准备好播放") case .fsAudioStreamFailed://
} print("\(loadPlayer.currentVideo?.title ?? "")加载失败")
}else { case .fsAudioStreamRetryingFailed://
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")") print("\(loadPlayer.currentVideo?.title ?? "")重试失败")
// print("失败URL:\(String(describing: loadPlayer.currentVideo?.resourcePlayerURL))")
loadPlayer.remakeImproveData { //
[weak self] in loadPlayer.remakeImproveData { [weak self] in
guard let self = self else {return} guard let self = self else {return}
// //
play() player?.url = loadPlayer.currentVideo.resourcePlayerURL! as NSURL
} }
} case .fsAudioStreamPlaying://
case "loadedTimeRanges":// ///
//Item print("\(loadPlayer.currentVideo?.title ?? "")开始播放")
if let timeRanges = loadPlayer.currentVideo?.resourcePlayerItem.loadedTimeRanges.map({$0.timeRangeValue}), let first = timeRanges.first {
//
let startSeconds = first.start.seconds
//
let durationSeconds = first.duration.seconds
//
let bufferedSeconds = startSeconds + durationSeconds
//
let maxDuration = getMusicDuration()
//
if cacheValueBlock != nil {
cacheValueBlock!(bufferedSeconds, maxDuration)
}
}
case "playbackLikelyToKeepUp"://
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
if playState != .Playing { if playState != .Playing {
//
player.play()
playState = .Playing playState = .Playing
// if startAction != nil {
if startActionBlock != nil { startAction!()
startActionBlock!()
} }
} }
}else { case .fsAudioStreamPlaybackCompleted://
// playerDidFinishPlaying()
player.pause() case .fsAudioStreamEndOfFile://
playState = .Null print("\(loadPlayer.currentVideo?.title ?? "")加载完毕")
default:
break
} }
default: }
break
}
///
private func findTurePlayer(_ stream:FSAudioStream) -> Bool {
guard let currentVideoURL = loadPlayer?.currentVideo?.resourcePlayerURL as? NSURL else {
return false
}
let streamURL = stream.url
if streamURL == currentVideoURL {
return true
}else {
return false
} }
} }
//MARK: -
/// ///
private func getMusicDuration() -> TimeInterval { private func preloadNext(_ url:URL) -> FSAudioStream{
return CMTimeGetSeconds(player.currentItem?.duration ?? .zero) let stream = FSAudioStream(url: url)
stream?.maxRetryCount = 1
//
stream!.preload()
print("下一首已经在预加载")
return stream!
} }
//MARK: - //MARK: -
// //
@objc private func playerDidFinishPlaying(_ sender:Notification) { @objc private func playerDidFinishPlaying() {
// //
guard playState == .Playing else { guard playState == .Playing else {
return return
} }
switch playType { switch playType {
case .single: case .single:
var postion = FSStreamPosition()
postion.position = 0
// //
player.seek(to: CMTime.zero) player.seek(to: postion)
player.play() player.play()
default: default:
// //
@ -310,7 +329,8 @@ class MP_PlayerManager:NSObject{
resumeAction!() resumeAction!()
} }
// //
player.play() // player.play()
player.pause()
// //
playState = .Playing playState = .Playing
} }
@ -323,7 +343,8 @@ class MP_PlayerManager:NSObject{
return return
} }
// //
player.play() // player.play()
player.pause()
// //
playState = .Playing playState = .Playing
} }
@ -337,7 +358,7 @@ class MP_PlayerManager:NSObject{
print("Player is not started") print("Player is not started")
return return
} }
player.pause() player.stop()
playState = .Null playState = .Null
} }
//MARK: - / //MARK: - /
@ -430,21 +451,21 @@ class MP_PlayerManager:NSObject{
@objc private func userSwitchCurrentVideoAction(_ sender:Notification) { @objc private func userSwitchCurrentVideoAction(_ sender:Notification) {
// //
playState = .Null playState = .Null
// //
player.pause() if player != nil {
// //
if let video = sender.object as? MPPositive_SongViewModel { stopAndReleaseStream(&player)
//KVO
video.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
video.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
} }
if cacheValueBlock != nil { if cacheValueBlock != nil {
cacheValueBlock!(0, 1) cacheValueBlock!(0)
} }
if loadPlayer.currentVideo != nil { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// [weak self] in
play(startAction: startActionBlock) guard let self = self else {return}
if loadPlayer.currentVideo != nil {
//
play(startAction: startActionBlock)
}
} }
} }
@ -460,22 +481,85 @@ class MP_PlayerManager:NSObject{
/// - Parameters: /// - Parameters:
/// - progress: 0-1 /// - progress: 0-1
func setEditProgressEnd(_ progress:Float, endAction:MP_PlayTimerEditEndAction? = nil) { func setEditProgressEnd(_ progress:Float, endAction:MP_PlayTimerEditEndAction? = nil) {
guard playState != .Null else { guard playState != .Null, let player = player, findTurePlayer(player) else {
return return
} }
guard progress >= 0, progress <= 1 else { guard progress >= 0, progress <= 1 else {
return return
} }
//
let timePoint:Double = Double(progress)*getMusicDuration()
// //
let time:CMTime = .init(seconds: timePoint, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) var time:FSStreamPosition = .init()
// time.position = progress
player.seek(to: time) //
// let currentTime = player.currentTimePlayed.playbackTimeInSeconds
resume() if progress != currentTime {
if endAction != nil { //
endAction!() player.seek(to: time)
//
resume()
if endAction != nil {
endAction!()
}
} }
} }
///
private func stopAndReleaseStream(_ stream: inout FSAudioStream?) {
stream?.stop() //
stream?.onStateChange = nil //
stream?.onFailure = nil
stream = nil
}
} }
////MARK: -
//extension MP_PlayerManager: MP_AVPlayerItemDelegate {
// ///
// func playerItemReadyToPlay(_ playerItem: MP_AVPlayerItem) {
// DispatchQueue.main.async {
// [weak self] in
// guard let self = self else {return}
// if playState != .Playing {
// //
// player.play()
// playState = .Playing
// //
// if startActionBlock != nil {
// startActionBlock!()
// }
//
// }
// }
// }
// ///
// func playerItem(_ playerItem: MP_AVPlayerItem, progress:Float) {
// DispatchQueue.main.async {
// [weak self] in
// guard let self = self else {return}
// //
// if cacheValueBlock != nil {
// cacheValueBlock!(progress)
// }
// }
// }
// ///
// func playerItem(_ playerItem: MP_AVPlayerItem, didFinishLoadingData data: Data) {
// print("\(loadPlayer.currentVideo.title ?? "") ")
// DispatchQueue.main.async {
// [weak self] in
// guard let self = self else {return}
// //
// if cacheValueBlock != nil {
// cacheValueBlock!(1)
// }
// }
// }
// ///,
// func playerItemPlaybackStalled(_ playerItem: MP_AVPlayerItem) {
// print("")
// }
// ///
// func playerItem(_ playerItem: MP_AVPlayerItem, loadingError error: any Error) {
// print("\(loadPlayer.currentVideo.title ?? "") ,---\(error)")
// }
//
//
//}

View File

@ -46,23 +46,31 @@ struct JsonPlayer: Codable {
struct Format: Codable { struct Format: Codable {
/// ///
let itag: Int? let itag: Int?
///
let bitrate:Float?
/// ///
let mimeType:String? let mimeType:String?
/// ///
let qualityLabel:String? let qualityLabel:String?
/// ///
let signatureCipher:String? let signatureCipher:String?
///
let approxDurationMs:String?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case itag = "itag" case itag = "itag"
case bitrate = "bitrate"
case mimeType = "mimeType" case mimeType = "mimeType"
case qualityLabel = "qualityLabel" case qualityLabel = "qualityLabel"
case signatureCipher = "signatureCipher" case signatureCipher = "signatureCipher"
case approxDurationMs = "approxDurationMs"
} }
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self) let values = try decoder.container(keyedBy: CodingKeys.self)
itag = try values.decodeIfPresent(Int.self, forKey: .itag) itag = try values.decodeIfPresent(Int.self, forKey: .itag)
bitrate = try values.decodeIfPresent(Float.self, forKey: .bitrate)
mimeType = try values.decodeIfPresent(String.self, forKey: .mimeType) mimeType = try values.decodeIfPresent(String.self, forKey: .mimeType)
qualityLabel = try values.decodeIfPresent(String.self, forKey: .qualityLabel) qualityLabel = try values.decodeIfPresent(String.self, forKey: .qualityLabel)
approxDurationMs = try values.decodeIfPresent(String.self, forKey: .approxDurationMs)
signatureCipher = try values.decodeIfPresent(String.self, forKey: .signatureCipher) signatureCipher = try values.decodeIfPresent(String.self, forKey: .signatureCipher)
} }
} }

View File

@ -13,8 +13,10 @@ class MPPositive_CollectionListModel: NSManagedObject, MP_CoreDataManageableDele
typealias ManagedObject = MPPositive_CollectionListModel typealias ManagedObject = MPPositive_CollectionListModel
/// ///
@NSManaged var coverImage:URL! @NSManaged var coverImage:URL!
/// ///
@NSManaged var title:String? @NSManaged var title:String?
///
@NSManaged var subtitle:String?
///ID ///ID
@NSManaged var browseId:String? @NSManaged var browseId:String?
/// ///

View File

@ -19,6 +19,8 @@ class MPPositive_CollectionSongModel: NSManagedObject, MP_CoreDataManageableDele
@NSManaged var subtitle:String? @NSManaged var subtitle:String?
///VideoID ///VideoID
@NSManaged var videoId:String? @NSManaged var videoId:String?
///ID
@NSManaged var lyricsID:String?
///ID
@NSManaged var relatedID:String?
} }

View File

@ -11,12 +11,10 @@ import CoreData
@objc(MPPositive_DownloadItemModel) @objc(MPPositive_DownloadItemModel)
class MPPositive_DownloadItemModel: NSManagedObject, MP_CoreDataManageableDelegate, MP_CoreDataOperationDelegate { class MPPositive_DownloadItemModel: NSManagedObject, MP_CoreDataManageableDelegate, MP_CoreDataOperationDelegate {
typealias ManagedObject = MPPositive_DownloadItemModel typealias ManagedObject = MPPositive_DownloadItemModel
///
@NSManaged var resourcePath:String!
/// ///
@NSManaged var coverImage:URL! @NSManaged var coverImage:String!
/// ///
@NSManaged var reviewImage:URL! @NSManaged var reviewImage:String!
/// ///
@NSManaged var title:String? @NSManaged var title:String?
///// /////
@ -24,9 +22,11 @@ class MPPositive_DownloadItemModel: NSManagedObject, MP_CoreDataManageableDelega
///() ///()
@NSManaged var lengthText:String? @NSManaged var lengthText:String?
/// ///
@NSManaged var shortBylineText:String? @NSManaged var shortBylineText:String?
/// ///
@NSManaged var lyrics:String? @NSManaged var lyrics:String?
///ID
@NSManaged var lyricsID:String?
///VideoID ///VideoID
@NSManaged var videoId:String! @NSManaged var videoId:String!
///ID ///ID

View File

@ -0,0 +1,18 @@
//
// MPPositive_SearchTagModel.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
import CoreData
////
@objc(MPPositive_SearchTagModel)
class MPPositive_SearchTagModel: NSManagedObject, MP_CoreDataManageableDelegate, MP_CoreDataOperationDelegate {
typealias ManagedObject = MPPositive_SearchTagModel
///
@NSManaged var date:Date!
///
@NSManaged var text:String!
}

View File

@ -10,11 +10,9 @@ import UIKit
class MPPositive_SongItemModel: NSObject { class MPPositive_SongItemModel: NSObject {
/// ///
var index:Int! var index:Int!
/// ///
var resourceUrls:[String]? var resourceUrls:[String]?
/// ///
var audioUrls:[String]?
///
var coverUrls:[String]? var coverUrls:[String]?
/// ///
var reviewUrls:[String]? var reviewUrls:[String]?
@ -28,9 +26,16 @@ class MPPositive_SongItemModel: NSObject {
var shortBylineText:String? var shortBylineText:String?
///ID ///ID
var lyricsID:String? var lyricsID:String?
///
var lyrics:String?
///VideoID ///VideoID
var videoId:String! var videoId:String!
///ID ///ID
var relatedID:String! var relatedID:String!
///
var bitrates:[Float]?
///
var approxDurationMs:[String]?
///ID
var playlistId:String?
} }

View File

@ -0,0 +1,40 @@
//
// MPPositive_CollectionArtistViewModel.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
import Kingfisher
///
class MPPositive_CollectionArtistViewModel: NSObject {
///
var coverURL:URL!
///
var title:String?
///
var subtitle:String?
///
var collectionArtist:MPPositive_CollectionArtistModel
init(_ artist:MPPositive_CollectionArtistModel) {
collectionArtist = artist
super.init()
configure()
}
//
private func configure() {
if collectionArtist.coverImage != nil {
coverURL = collectionArtist.coverImage
}
title = collectionArtist.title
subtitle = collectionArtist.subtitle
}
//
func setImage(_ imageView:UIImageView) {
if coverURL != nil {
imageView.kf.setImage(with: coverURL, placeholder: placeholderImage)
}
}
}

View File

@ -0,0 +1,40 @@
//
// MPPositive_CollectionListViewModel.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
import Kingfisher
///ViewModel
class MPPositive_CollectionListViewModel: NSObject {
///
var coverURL:URL!
///
var title:String?
///
var subtitle:String?
///
var collectionList:MPPositive_CollectionListModel
init(_ list:MPPositive_CollectionListModel) {
collectionList = list
super.init()
configure()
}
//
private func configure() {
if collectionList.coverImage != nil {
coverURL = collectionList.coverImage
}
title = collectionList.title
subtitle = collectionList.subtitle
}
//
func setImage(_ imageView:UIImageView) {
if coverURL != nil {
imageView.kf.setImage(with: coverURL, placeholder: placeholderImage)
}
}
}

View File

@ -0,0 +1,40 @@
//
// MPPositive_CollectionSongViewModel.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
import Kingfisher
///ViewModel
class MPPositive_CollectionSongViewModel: NSObject {
///
var coverURL:URL!
///
var title:String?
///
var subtitle:String?
///
var collectionSong:MPPositive_CollectionSongModel
init(_ song:MPPositive_CollectionSongModel) {
collectionSong = song
super.init()
configure()
}
//
private func configure() {
if collectionSong.coverImage != nil {
coverURL = collectionSong.coverImage
}
title = collectionSong.title
subtitle = collectionSong.subtitle
}
//
func setImage(_ imageView:UIImageView) {
if coverURL != nil {
imageView.kf.setImage(with: coverURL, placeholder: placeholderImage)
}
}
}

View File

@ -0,0 +1,39 @@
//
// MPPositive_DownloadViewModel.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
import Kingfisher
///ViewModel
class MPPositive_DownloadViewModel: NSObject {
///
var reviewURL:URL?
///
var title:String?
///
var subtitle:String?
///
var loadItem:MPPositive_DownloadItemModel
init(_ loadItem:MPPositive_DownloadItemModel) {
self.loadItem = loadItem
super.init()
configure()
}
//
private func configure() {
if let url = URL(string: loadItem.reviewImage) {
reviewURL = url
}
title = loadItem.title
subtitle = loadItem.shortBylineText
}
//
func setImage(_ imageView:UIImageView) {
if reviewURL != nil {
imageView.kf.setImage(with: reviewURL, placeholder: placeholderImage)
}
}
}

View File

@ -8,13 +8,20 @@
import UIKit import UIKit
import AVKit import AVKit
import AVFoundation import AVFoundation
import FreeStreamer
class MPPositive_SongViewModel: NSObject { class MPPositive_SongViewModel: NSObject {
/// ///
var index:Int! var index:Int!
/// ///
var resourcePlayerItem:AVPlayerItem! // var resourcePlayerItem:AVPlayerItem!
// var resourcePlayerItem:MP_AVPlayerItem!
// var resourcePlayerItem:CachingPlayerItem!
///
// var resourcePlayerItem:FSAudioStream?
///
var resourcePlayerURL:URL?
/// ///
var resourceAsset:AVURLAsset! // var resourceAsset:MP_AVURLAsset!
/// ///
var coverUrl:URL? var coverUrl:URL?
/// ///
@ -31,31 +38,28 @@ class MPPositive_SongViewModel: NSObject {
var isDlownd:Bool? var isDlownd:Bool?
/// ///
var song:MPPositive_SongItemModel! var song:MPPositive_SongItemModel!
///
var isPloading:Bool = false
//
private var isCancelled = false
init(_ song:MPPositive_SongItemModel) { init(_ song:MPPositive_SongItemModel) {
super.init() super.init()
self.song = song self.song = song
// resourcePlayerItem = nil
configure() configure()
} }
deinit { deinit {
// resourcePlayerURL = nil
resourcePlayerItem = nil
resourceAsset = nil
isCancelled = true
print("\(title ?? "")被释放了")
} }
// //
private func configure() { func configure() {
reloadCollectionAndDownLoad()
index = song.index index = song.index
//
if song.resourceUrls?.first != nil, let first = URL(string: song.resourceUrls?.first ?? ""){ if let first = song.resourceUrls?.first {
// //
resourceAsset = .init(url: first, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]) if isDlownd == true {
//PlayerItem resourcePlayerURL = .init(string:first)
resourcePlayerItem = .init(asset: resourceAsset) }else {
//使
resourcePlayerURL = .init(string: first)
}
} }
// //
if song.reviewUrls?.first != nil { if song.reviewUrls?.first != nil {
@ -70,21 +74,21 @@ class MPPositive_SongViewModel: NSObject {
subtitle = song.shortBylineText! subtitle = song.shortBylineText!
} }
// //
if song.lyricsID != nil { if song.lyrics != nil {
// lyrics = song.lyrics
MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in }else {
self?.lyrics = lyrics if song.lyricsID != nil {
//
MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in
self?.lyrics = lyrics
self?.song.lyrics = lyrics
}
} }
} }
// //
if song.relatedID != nil { if song.relatedID != nil {
relatedId = song.relatedID relatedId = song.relatedID
} }
reloadCollectionAndDownLoad()
//
// if isPloading == false {
// preloadAsset(resourceAsset)
// }
} }
// //
func reloadCollectionAndDownLoad() { func reloadCollectionAndDownLoad() {
@ -94,66 +98,4 @@ class MPPositive_SongViewModel: NSObject {
// //
isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0 isDlownd = MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", song.videoId)).count != 0
} }
//
func preloadAsset(_ asset:AVURLAsset) {
guard isPloading == false else {
return
}
self.isPloading = true
//
if #available(iOS 16, *) {
//ios16
Task{
do{
let playable = try await asset.load(.isPlayable)
guard !isCancelled else {
return
}
if playable == true {
print("\(self.title ?? "")预加载成功")
self.isPloading = true
}else {
//
switch asset.status(of: .isPlayable) {
case .failed(let erro):
print("\(title ?? "")预加载失败,失败原因:\(erro.localizedDescription)")
self.isPloading = false
default:
self.isPloading = false
}
}
}catch{
print("预加载失败:\(error.localizedDescription)")
}
}
}else {
//ios16
let keys = ["playable"]
asset.loadValuesAsynchronously(forKeys: keys) {
[weak self] in
guard let self = self, !isCancelled else {return}
for key in keys {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: key, error: &error)
switch status {
case .loaded:
// key
DispatchQueue.main.async {
print("\(self.title ?? "")预加载成功")
self.isPloading = true
}
case .failed:
print("\(title ?? "")预加载失败,失败原因:\(error?.localizedDescription ?? "")")
self.isPloading = false
case .cancelled:
print("\(title ?? "")预加载被取消了")
self.isPloading = false
default:
break
}
}
}
}
}
} }

View File

@ -0,0 +1,91 @@
//
// MPPositive_LoadCoreModel.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
///
class MPPositive_LoadCoreModel: NSObject {
static let shared = MPPositive_LoadCoreModel()
///ViewModel
var listViewModels:[MPPositive_CollectionListViewModel] = []
///ViewModel
var artistViewModels:[MPPositive_CollectionArtistViewModel] = []
///ViewModel
var songViewModels:[MPPositive_CollectionSongViewModel] = []
///ViewModel
var loadViewModels:[MPPositive_DownloadViewModel] = []
///
var searchTags:[MPPositive_SearchTagModel] = []
private override init() {
super.init()
reloadCollectionListViewModels(nil)
reloadCollectionArtistViewModels(nil)
reloadCollectionSongViewModel(nil)
reloadLoadSongViewModel(nil)
reloadSearchTags(nil)
}
///
func reloadCollectionListViewModels(_ complection:(() -> Void)?) {
listViewModels = []
let array = MPPositive_CollectionListModel.fetchAll()
array.forEach { item in
listViewModels.append(.init(item))
}
//
listViewModels = listViewModels.reversed()
if complection != nil {
complection!()
}
}
///
func reloadCollectionArtistViewModels(_ complection:(() -> Void)?) {
artistViewModels = []
let array = MPPositive_CollectionArtistModel.fetchAll()
array.forEach { item in
artistViewModels.append(.init(item))
}
//
artistViewModels = artistViewModels.reversed()
if complection != nil {
complection!()
}
}
///
func reloadCollectionSongViewModel(_ complection:(() -> Void)?) {
songViewModels = []
let array = MPPositive_CollectionSongModel.fetchAll()
array.forEach { item in
songViewModels.append(.init(item))
}
//
songViewModels = songViewModels.reversed()
if complection != nil {
complection!()
}
}
///
func reloadLoadSongViewModel(_ complection:(() -> Void)?) {
loadViewModels = []
let array = MPPositive_DownloadItemModel.fetchAll()
array.forEach { item in
loadViewModels.append(.init(item))
}
//
loadViewModels = loadViewModels.reversed()
if complection != nil {
complection!()
}
}
///
func reloadSearchTags(_ complection:(() -> Void)?) {
searchTags = []
searchTags = MPPositive_SearchTagModel.fetchAll().sorted(by: {$1.date > $0.date})
if complection != nil {
complection!()
}
}
}

View File

@ -43,7 +43,7 @@ class MPPositive_PlayerLoadViewModel: NSObject {
self.listViewVideos = [] self.listViewVideos = []
} }
///Video23VideoViewModel, ///Video13VideoViewModel,
func improveData(_ targetVideoId:String, isRandom:Bool = false) { func improveData(_ targetVideoId:String, isRandom:Bool = false) {
//Video //Video
var array:[MPPositive_SongItemModel] = [] var array:[MPPositive_SongItemModel] = []
@ -82,25 +82,41 @@ class MPPositive_PlayerLoadViewModel: NSObject {
let videoIDs = Set(listViewVideos.map({$0.song.videoId})) let videoIDs = Set(listViewVideos.map({$0.song.videoId}))
//videoID, //videoID,
array = array.filter({!videoIDs.contains($0.videoId)}) array = array.filter({!videoIDs.contains($0.videoId)})
group = DispatchGroup() group = DispatchGroup()
//, //,
for item in array { for item in array {
group?.enter() group?.enter()
//idid //idid
improveDataforLycirsAndRelated(item) {[weak self] (result) in if item.lyricsID == nil || item.relatedID == nil {
item.lyricsID = result.0 improveDataforLycirsAndRelated(item) {[weak self] (result) in
item.relatedID = result.1 item.lyricsID = result.0
self?.group?.leave() item.relatedID = result.1
self?.group?.leave()
}
}else {
group?.leave()
} }
group?.enter() group?.enter()
// //videoID
improveDataforResouceAndCover(item) {[weak self] resourceUrls, coverUrls in if let resource = getDocumentsFileURL(item.videoId) {
item.resourceUrls = resourceUrls.1 //resource
item.audioUrls = resourceUrls.0 item.resourceUrls = [resource]
item.coverUrls = coverUrls
//,ViewModellistViewVideos //,ViewModellistViewVideos
self?.listViewVideos.append(.init(item)) listViewVideos.append(.init(item))
self?.group?.leave() group?.leave()
}else {
//
//
improveDataforResouceAndCover(item) {[weak self] resourceUrls, coverUrls in
item.resourceUrls = resourceUrls.0
item.bitrates = resourceUrls.1
item.approxDurationMs = resourceUrls.2
item.coverUrls = coverUrls
//,ViewModellistViewVideos
self?.listViewVideos.append(.init(item))
self?.group?.leave()
}
} }
} }
group?.notify(queue: .main, execute: { group?.notify(queue: .main, execute: {
@ -117,17 +133,21 @@ class MPPositive_PlayerLoadViewModel: NSObject {
// //
improveDataforResouceAndCover(currentVideo.song) {[weak self] resourceUrls, coverUrls in improveDataforResouceAndCover(currentVideo.song) {[weak self] resourceUrls, coverUrls in
guard let self = self else {return} guard let self = self else {return}
currentVideo.song.resourceUrls = resourceUrls.1 currentVideo.song.resourceUrls = resourceUrls.0
currentVideo.song.audioUrls = resourceUrls.0 currentVideo.song.bitrates = resourceUrls.1
currentVideo.song.approxDurationMs = resourceUrls.2
//listViewVideos //listViewVideos
listViewVideos.forEach({ item in listViewVideos.forEach({ item in
if item.song.videoId == self.currentVideo.song.videoId { if item.song.videoId == self.currentVideo.song.videoId {
item.song.resourceUrls = self.currentVideo.song.resourceUrls item.song.resourceUrls = self.currentVideo.song.resourceUrls
item.song.audioUrls = self.currentVideo.song.audioUrls item.song.bitrates = self.currentVideo.song.bitrates
item.song.approxDurationMs = self.currentVideo.song.approxDurationMs
} }
}) })
currentVideo.resourceAsset = .init(url: .init(string: currentVideo.song.resourceUrls!.first!)!) // currentVideo.resourceAsset = .init(url: .init(string: currentVideo.song.resourceUrls!.first!)!)
currentVideo.resourcePlayerItem = .init(asset: currentVideo.resourceAsset!) // currentVideo.resourcePlayerItem = .init(asset: currentVideo.resourceAsset!)
// currentVideo.resourcePlayerItem = .init(url: .init(string: (currentVideo.song.resourceUrls?.first ?? ""))!, bitrate: Int64(currentVideo.song.bitrates?.first ?? 0), title: currentVideo.title, videoId: currentVideo.song.videoId)
currentVideo.configure()
//UI //UI
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload) NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
completion() completion()
@ -156,6 +176,11 @@ class MPPositive_PlayerLoadViewModel: NSObject {
} }
} }
} }
//videoId
private func findVideoIdForDocument(_ videoId:String) -> Bool {
return MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).count != 0
}
///nextIDID ///nextIDID
private func improveDataforLycirsAndRelated(_ song:MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) { private func improveDataforLycirsAndRelated(_ song:MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) {
//next //next
@ -164,9 +189,9 @@ class MPPositive_PlayerLoadViewModel: NSObject {
} }
} }
///player ///player
private func improveDataforResouceAndCover(_ song:MPPositive_SongItemModel, completion:@escaping((([String],[String]), [String]?) -> Void)) { private func improveDataforResouceAndCover(_ song:MPPositive_SongItemModel, completion:@escaping((([String],[Float],[String]), [String]?) -> Void)) {
//player //player
MP_NetWorkManager.shared.requestPlayer(song) { resourceUrls, coverUrls in MP_NetWorkManager.shared.requestPlayer(song.videoId, playlistId: song.playlistId ?? "") { resourceUrls, coverUrls in
completion(resourceUrls,coverUrls) completion(resourceUrls,coverUrls)
} }
} }

View File

@ -29,6 +29,12 @@ class MPPositive_SearchResultsLoadViewModel: NSObject {
} }
// //
private func getSearchResults(_ text:String) { private func getSearchResults(_ text:String) {
//
let tag = MPPositive_SearchTagModel.create()
tag.date = Date().timeZone()
tag.text = text
MPPositive_SearchTagModel.save()
MPPositive_LoadCoreModel.shared.reloadSearchTags(nil)
MP_NetWorkManager.shared.requestSearchPreviewResults(text) { [weak self] results in MP_NetWorkManager.shared.requestSearchPreviewResults(text) { [weak self] results in
self?.sectionLists = results self?.sectionLists = results
} }

View File

@ -13,6 +13,7 @@ class MPPositive_NavigationController: MP_NavigationController {
//pushpushviewControllerhidesBottomBarWhenPushed //pushpushviewControllerhidesBottomBarWhenPushed
override func pushViewController(_ viewController: UIViewController, animated: Bool) { override func pushViewController(_ viewController: UIViewController, animated: Bool) {
viewController.hidesBottomBarWhenPushed = true viewController.hidesBottomBarWhenPushed = true
NotificationCenter.notificationKey.post(notificationName: .positive_nav_push)
super.pushViewController(viewController, animated: true) super.pushViewController(viewController, animated: true)
viewController.hidesBottomBarWhenPushed = false viewController.hidesBottomBarWhenPushed = false
} }
@ -30,13 +31,14 @@ class MPPositive_NavigationController: MP_NavigationController {
let count = self.children.count-2 let count = self.children.count-2
let controller = self.children[count] let controller = self.children[count]
controller.hidesBottomBarWhenPushed = true controller.hidesBottomBarWhenPushed = true
NotificationCenter.notificationKey.post(notificationName: .sideA_hidden_show) NotificationCenter.notificationKey.post(notificationName: .positive_nav_push)
return super.popViewController(animated: true) return super.popViewController(animated: true)
} }
//viewControllerViewController //viewControllerViewController
//tabbar //tabbar
let controller:UIViewController = self.children[0] let controller:UIViewController = self.children[0]
controller.hidesBottomBarWhenPushed = false controller.hidesBottomBarWhenPushed = false
NotificationCenter.notificationKey.post(notificationName: .positive_nav_pop)
return super.popViewController(animated: true) return super.popViewController(animated: true)
} }
} }

View File

@ -11,6 +11,14 @@ class MPPositive_TabBarController: UITabBarController, UIViewControllerTransitio
//tabBar //tabBar
private lazy var customTabBar:MPPositive_CustomTabBar = .init(frame: .init(x: 0, y: 0, width: screen_Width, height: 72*width)) private lazy var customTabBar:MPPositive_CustomTabBar = .init(frame: .init(x: 0, y: 0, width: screen_Width, height: 72*width))
private lazy var bottomView:MPPositive_BottomShowView = .init(frame: .init(x: 0, y: 0, width: 351, height: 82)) private lazy var bottomView:MPPositive_BottomShowView = .init(frame: .init(x: 0, y: 0, width: 351, height: 82))
//
private var isbottomShow:Bool = false{
willSet{
if isbottomShow != newValue {
switch_bottomShowAnimation(newValue)
}
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
self.setValue(customTabBar, forKey: "tabBar") self.setValue(customTabBar, forKey: "tabBar")
@ -57,6 +65,8 @@ class MPPositive_TabBarController: UITabBarController, UIViewControllerTransitio
NotificationCenter.notificationKey.add(observer: self, selector: #selector(pupPlayerAction), notificationName: .pup_player_vc) NotificationCenter.notificationKey.add(observer: self, selector: #selector(pupPlayerAction), notificationName: .pup_player_vc)
NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .pup_bottom_show) NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .pup_bottom_show)
NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .player_delete_list) NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .player_delete_list)
NotificationCenter.notificationKey.add(observer: self, selector: #selector(pushAction(_ :)), notificationName: .positive_nav_push)
NotificationCenter.notificationKey.add(observer: self, selector: #selector(popAction(_ :)), notificationName: .positive_nav_pop)
} }
deinit { deinit {
// //
@ -84,7 +94,7 @@ extension MPPositive_TabBarController {
let playerVC = MPPositive_PlayerViewController() let playerVC = MPPositive_PlayerViewController()
playerVC.modalPresentationStyle = .fullScreen playerVC.modalPresentationStyle = .fullScreen
playerVC.recommendBlock = { playerVC.recommendBlock = {
let recommendVC = MPPositive_RecommendViewController(MP_PlayerManager.shared.loadPlayer.currentVideo.relatedId ?? "") let recommendVC = MPPositive_RecommendViewController(MP_PlayerManager.shared.loadPlayer.currentVideo.song.relatedID)
self?.viewControllers![self?.selectedIndex ?? 0].children[0].navigationController?.pushViewController(recommendVC, animated: true) self?.viewControllers![self?.selectedIndex ?? 0].children[0].navigationController?.pushViewController(recommendVC, animated: true)
} }
self?.present(playerVC, animated: true) self?.present(playerVC, animated: true)
@ -93,7 +103,7 @@ extension MPPositive_TabBarController {
} }
// //
@objc private func bottomAnimationAction(_ sender:Notification) { @objc private func bottomAnimationAction(_ sender:Notification) {
switch_bottomShowAnimation(MP_PlayerManager.shared.loadPlayer != nil) isbottomShow = MP_PlayerManager.shared.loadPlayer != nil
} }
//BottomView //BottomView
private func switch_bottomShowAnimation(_ state:Bool) { private func switch_bottomShowAnimation(_ state:Bool) {
@ -109,4 +119,27 @@ extension MPPositive_TabBarController {
} }
} }
} }
//push
@objc private func pushAction(_ sender:Notification) {
//
if isbottomShow == true {
//bottomView83
UIView.animate(withDuration: 0.3) {
[weak self] in
guard let self = self else { return }
bottomView.transform = .init(translationX: 0, y: -82*width)
}
}
}
@objc private func popAction(_ sender:Notification) {
//
if isbottomShow == true {
//bottomView83
UIView.animate(withDuration: 0.3) {
[weak self] in
guard let self = self else { return }
bottomView.transform = .init(translationX: 0, y: -145*width)
}
}
}
} }

View File

@ -7,23 +7,219 @@
import UIKit import UIKit
class MPPositive_LibraryViewController: UIViewController { class MPPositive_LibraryViewController: MPPositive_BaseViewController {
//
private lazy var topImageView:UIImageView = {
let imageView:UIImageView = .init(image: .init(named: "Center_Top_bg"))
imageView.contentMode = .scaleAspectFill
return imageView
}()
//View
private lazy var blurView:UIVisualEffectView = setBlurView()
//View
private lazy var maskImageView:UIImageView = {
let imageView:UIImageView = .init(image: .init(named: "Player'mask"))
imageView.contentMode = .scaleAspectFill
return imageView
}()
//Label
private lazy var titleLabel:UILabel = createLabel("Your Library", font: .systemFont(ofSize: 32*width, weight: .regular), textColor: .white, textAlignment: .left)
//
private lazy var addBtn:UIButton = {
let btn:UIButton = UIButton()
btn.setBackgroundImage(UIImage(named: "Center_Add_'logo"), for: .normal)
btn.addTarget(self, action: #selector(addClick(_:)), for: .touchUpInside)
return btn
}()
///Label
private lazy var artistsLabel:UILabel = createLabel("0", font: .systemFont(ofSize: 12*width, weight: .light), textColor: .white, textAlignment: .center)
///Label
private lazy var songsLabel:UILabel = createLabel("0", font: .systemFont(ofSize: 12*width, weight: .light), textColor: .white, textAlignment: .center)
///Label
private lazy var loadsLabel:UILabel = createLabel("0", font: .systemFont(ofSize: 12*width, weight: .light), textColor: .white, textAlignment: .center)
///View
private lazy var actionViews:UIView = showTopView()
///tableView
private lazy var tableView:UITableView = {
let tableView = UITableView(frame: .init(x: 0, y: 0, width: screen_Width, height: screen_Height), style: .plain)
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 200
tableView.rowHeight = UITableView.automaticDimension
tableView.dataSource = self
tableView.delegate = self
tableView.register(MPPositive_LibraryTableViewCell.self, forCellReuseIdentifier: MPPositive_LibraryTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView
}()
private let MPPositive_LibraryTableViewCellID = "MPPositive_LibraryTableViewCell"
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setTitle("")
configure()
view.backgroundColor = .init(hex: "#000000")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reload()
}
//
private func reload() {
MPPositive_LoadCoreModel.shared.reloadCollectionListViewModels {
[weak self] in
guard let self = self else {return}
tableView.showMessage(MPPositive_LoadCoreModel.shared.listViewModels.count, title: "No Lists")
artistsLabel.text = "\(MPPositive_LoadCoreModel.shared.artistViewModels.count)"
songsLabel.text = "\(MPPositive_LoadCoreModel.shared.songViewModels.count)"
loadsLabel.text = "\(MPPositive_LoadCoreModel.shared.loadViewModels.count)"
tableView.reloadData()
}
}
///
@objc private func addClick(_ sender:UIButton) {
// Do any additional setup after loading the view. }
private func configure() {
view.addSubview(topImageView)
topImageView.snp.makeConstraints { make in
make.left.top.right.equalToSuperview()
make.height.equalTo(291*width)
}
view.addSubview(blurView)
blurView.snp.makeConstraints { make in
make.left.top.right.equalToSuperview()
make.height.equalTo(topImageView)
}
view.addSubview(maskImageView)
maskImageView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.bottom.equalTo(topImageView)
make.height.equalTo(109*width)
}
navView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(18*width)
make.centerY.equalToSuperview()
}
view.addSubview(actionViews)
actionViews.snp.makeConstraints { make in
make.top.equalTo(navView.snp.bottom).offset(20*width)
make.left.right.equalToSuperview()
make.height.equalTo(70*width)
}
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(topImageView.snp.bottom).offset(-55*width)
}
//Label
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 20, weight: .regular)
label.text = "Love Lists"
view.addSubview(label)
label.snp.makeConstraints { make in
make.left.equalToSuperview().offset(18*width)
make.bottom.equalTo(tableView.snp.top).offset(-10*width)
}
}
///View
private func setBlurView() -> UIVisualEffectView {
//
let blurEffect = UIBlurEffect(style: .dark)
//
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.alpha = 0.7
blurEffectView.isUserInteractionEnabled = true
return blurEffectView
} }
//View
/* private func showTopView() -> UIView {
// MARK: - Navigation let topView = UIView()
topView.backgroundColor = .clear
// In a storyboard-based application, you will often want to do a little preparation before navigation //
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let first = actionView(artistsLabel, text: "Love_Artists_logo", tag: 0)
// Get the new view controller using segue.destination. topView.addSubview(first)
// Pass the selected object to the new view controller. first.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.height.equalTo(67*width)
make.width.equalTo(90*width)
make.centerX.equalToSuperview().multipliedBy(0.35)
}
let second = actionView(songsLabel, text: "Love_Song_logo", tag: 1)
topView.addSubview(second)
second.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.height.equalTo(67*width)
make.width.equalTo(90*width)
make.centerX.equalToSuperview()
}
let third = actionView(loadsLabel, text: "Offline_Songs_logo", tag: 2)
topView.addSubview(third)
third.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.height.equalTo(67*width)
make.width.equalTo(90*width)
make.centerX.equalToSuperview().multipliedBy(1.65)
}
topView.isUserInteractionEnabled = true
return topView
}
//View
private func actionView(_ label:UILabel, text:String, tag:Int) -> UIView {
let actionView = UIView()
actionView.backgroundColor = .clear
let imageView:UIImageView = .init(image: .init(named: text))
imageView.contentMode = .scaleAspectFill
actionView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.top.centerX.equalToSuperview()
}
actionView.addSubview(label)
label.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.8)
make.bottom.equalToSuperview()
}
actionView.tag = tag
actionView.isUserInteractionEnabled = true
actionView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(actionClick(_:))))
return actionView
}
//
@objc private func actionClick(_ sender:UITapGestureRecognizer) {
let tag = sender.view?.tag
switch tag {
case 0:
let loveArtistsVC = MPPositive_LoveArtistsViewController()
navigationController?.pushViewController(loveArtistsVC, animated: true)
case 1:
let loveSongsVC = MPPositive_LoveSongsViewController()
navigationController?.pushViewController(loveSongsVC, animated: true)
default:
let offlineVC = MPPositive_OfflineSongsViewController()
navigationController?.pushViewController(offlineVC, animated: true)
}
}
}
//MARK: - tableView
extension MPPositive_LibraryViewController:UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MPPositive_LoadCoreModel.shared.listViewModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_LibraryTableViewCellID, for: indexPath) as! MPPositive_LibraryTableViewCell
cell.listViewModel = MPPositive_LoadCoreModel.shared.listViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = MPPositive_LoadCoreModel.shared.listViewModels[indexPath.row]
//
let listVC = MPPositive_ListShowViewController(item.collectionList.browseId ?? "", params: item.collectionList.params ?? "", title: item.title ?? "", subtitle: item.subtitle ?? "")
navigationController?.pushViewController(listVC, animated: true)
} }
*/
} }

View File

@ -0,0 +1,78 @@
//
// MPPositive_LoveArtistsViewController.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
class MPPositive_LoveArtistsViewController: MPPositive_BaseViewController {
private lazy var numbersLabel:UILabel = createLabel(font: .systemFont(ofSize: 18*width, weight: .regular), textColor: .white, textAlignment: .left)
///tableView
private lazy var tableView:UITableView = {
let tableView = UITableView(frame: .init(x: 0, y: 0, width: screen_Width, height: screen_Height), style: .plain)
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 200
tableView.rowHeight = UITableView.automaticDimension
tableView.dataSource = self
tableView.delegate = self
tableView.register(MPPositive_LoveArtistTableViewCell.self, forCellReuseIdentifier: MPPositive_LoveArtistTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView
}()
private let MPPositive_LoveArtistTableViewCellID = "MPPositive_LoveArtistTableViewCell"
override func viewDidLoad() {
super.viewDidLoad()
setTitle("Love Artists")
setPopBtn()
configure()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reload()
}
//
private func reload() {
MPPositive_LoadCoreModel.shared.reloadCollectionArtistViewModels {
[weak self] in
guard let self = self else {return}
tableView.showMessage(MPPositive_LoadCoreModel.shared.artistViewModels.count, title: "No Artists")
numbersLabel.text = "\(MPPositive_LoadCoreModel.shared.artistViewModels.count) Artists"
tableView.reloadData()
}
}
private func configure() {
view.addSubview(numbersLabel)
numbersLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(18*width)
make.top.equalTo(navView.snp.bottom).offset(32*width)
}
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.equalTo(navView.snp.bottom).offset(70*width)
make.left.right.bottom.equalToSuperview()
}
}
}
//MARK: - tableView
extension MPPositive_LoveArtistsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MPPositive_LoadCoreModel.shared.artistViewModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_LoveArtistTableViewCellID, for: indexPath) as! MPPositive_LoveArtistTableViewCell
cell.artistViewModel = MPPositive_LoadCoreModel.shared.artistViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = MPPositive_LoadCoreModel.shared.artistViewModels[indexPath.row]
//
let artistVC = MPPositive_ArtistShowViewController(item.collectionArtist.artistId ?? "")
navigationController?.pushViewController(artistVC, animated: true)
}
}

View File

@ -0,0 +1,92 @@
//
// MPPositive_LoveSongsViewController.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
class MPPositive_LoveSongsViewController: MPPositive_BaseViewController {
private lazy var numbersLabel:UILabel = createLabel(font: .systemFont(ofSize: 18*width, weight: .regular), textColor: .white, textAlignment: .left)
///tableView
private lazy var tableView:UITableView = {
let tableView = UITableView(frame: .init(x: 0, y: 0, width: screen_Width, height: screen_Height), style: .plain)
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 200
tableView.rowHeight = UITableView.automaticDimension
tableView.dataSource = self
tableView.delegate = self
tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView
}()
private let MPPositive_SearchResultShowTableViewCellID = "MPPositive_SearchResultShowTableViewCell"
override func viewDidLoad() {
super.viewDidLoad()
setTitle("Love Songs")
setPopBtn()
configure()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reload()
}
//
private func reload() {
MPPositive_LoadCoreModel.shared.reloadCollectionSongViewModel {
[weak self] in
guard let self = self else {return}
tableView.showMessage(MPPositive_LoadCoreModel.shared.songViewModels.count, title: "No Songs")
numbersLabel.text = "\(MPPositive_LoadCoreModel.shared.songViewModels.count) Songs"
tableView.reloadData()
}
}
private func configure() {
view.addSubview(numbersLabel)
numbersLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(18*width)
make.top.equalTo(navView.snp.bottom).offset(32*width)
}
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.equalTo(navView.snp.bottom).offset(70*width)
make.left.right.bottom.equalToSuperview()
}
}
}
//MARK: - tableView
extension MPPositive_LoveSongsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MPPositive_LoadCoreModel.shared.songViewModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchResultShowTableViewCellID, for: indexPath) as! MPPositive_SearchResultShowTableViewCell
cell.songViewModel = MPPositive_LoadCoreModel.shared.songViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//
var array:[MPPositive_SongItemModel] = []
for (index,song) in MPPositive_LoadCoreModel.shared.songViewModels.enumerated() {
let item = MPPositive_SongItemModel()
//item
item.index = index
item.reviewUrls = [song.coverURL.absoluteString]
item.title = song.title
item.shortBylineText = song.subtitle
item.videoId = song.collectionSong.videoId
item.lyricsID = song.collectionSong.lyricsID
item.relatedID = song.collectionSong.relatedID
array.append(item)
}
let lodaViewModel = MPPositive_PlayerLoadViewModel(array, currentVideoId: MPPositive_LoadCoreModel.shared.songViewModels[indexPath.row].collectionSong.videoId ?? "")
lodaViewModel.improveData(MPPositive_LoadCoreModel.shared.songViewModels[indexPath.row].collectionSong.videoId ?? "")
MP_PlayerManager.shared.loadPlayer = lodaViewModel
NotificationCenter.notificationKey.post(notificationName: .pup_player_vc)
}
}

View File

@ -0,0 +1,95 @@
//
// MPPositive_OfflineSongsViewController.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
class MPPositive_OfflineSongsViewController: MPPositive_BaseViewController {
private lazy var numbersLabel:UILabel = createLabel(font: .systemFont(ofSize: 18*width, weight: .regular), textColor: .white, textAlignment: .left)
///tableView
private lazy var tableView:UITableView = {
let tableView = UITableView(frame: .init(x: 0, y: 0, width: screen_Width, height: screen_Height), style: .plain)
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 200
tableView.rowHeight = UITableView.automaticDimension
tableView.dataSource = self
tableView.delegate = self
tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView
}()
private let MPPositive_SearchResultShowTableViewCellID = "MPPositive_SearchResultShowTableViewCell"
override func viewDidLoad() {
super.viewDidLoad()
setTitle("Offline Songs")
setPopBtn()
configure()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reload()
}
//
private func reload() {
MPPositive_LoadCoreModel.shared.reloadLoadSongViewModel {
[weak self] in
guard let self = self else {return}
tableView.showMessage(MPPositive_LoadCoreModel.shared.loadViewModels.count, title: "No Songs")
numbersLabel.text = "\(MPPositive_LoadCoreModel.shared.loadViewModels.count) Songs"
tableView.reloadData()
}
}
private func configure() {
view.addSubview(numbersLabel)
numbersLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(18*width)
make.top.equalTo(navView.snp.bottom).offset(32*width)
}
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.equalTo(navView.snp.bottom).offset(70*width)
make.left.right.bottom.equalToSuperview()
}
}
}
//MARK: - tableView
extension MPPositive_OfflineSongsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MPPositive_LoadCoreModel.shared.loadViewModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchResultShowTableViewCellID, for: indexPath) as! MPPositive_SearchResultShowTableViewCell
cell.loadViewModel = MPPositive_LoadCoreModel.shared.loadViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//
var array:[MPPositive_SongItemModel] = []
for (index, song) in MPPositive_LoadCoreModel.shared.loadViewModels.enumerated() {
let item = MPPositive_SongItemModel()
item.index = index
item.coverUrls = [song.loadItem.coverImage]
item.reviewUrls = [song.loadItem.reviewImage]
item.title = song.loadItem.title
item.longBylineText = song.loadItem.longBylineText
item.lengthText = song.loadItem.lengthText
item.shortBylineText = song.loadItem.shortBylineText
item.lyricsID = song.loadItem.lyricsID
item.lyrics = song.loadItem.lyrics
item.videoId = song.loadItem.videoId
item.relatedID = song.loadItem.relatedID
array.append(item)
}
let lodaViewModel = MPPositive_PlayerLoadViewModel(array, currentVideoId: MPPositive_LoadCoreModel.shared.loadViewModels[indexPath.row].loadItem.videoId ?? "")
lodaViewModel.improveData(MPPositive_LoadCoreModel.shared.loadViewModels[indexPath.row].loadItem.videoId ?? "")
MP_PlayerManager.shared.loadPlayer = lodaViewModel
NotificationCenter.notificationKey.post(notificationName: .pup_player_vc)
}
}

View File

@ -178,7 +178,7 @@ extension MPPositive_ArtistShowViewController: JXPagingViewDelegate{
navigationController?.pushViewController(artistVC, animated: true) navigationController?.pushViewController(artistVC, animated: true)
case .list: case .list:
// //
let listVC = MPPositive_ListShowViewController(item.browseItem.browseId ?? "", params: "") let listVC = MPPositive_ListShowViewController(item.browseItem.browseId ?? "", params: "", title: item.title ?? "", subtitle: item.subtitle ?? "")
navigationController?.pushViewController(listVC, animated: true) navigationController?.pushViewController(listVC, animated: true)
case .single: case .single:
/// ///

View File

@ -31,6 +31,7 @@ class MPPositive_HomeViewController: MPPositive_BaseViewController{
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.register(MPPositive_HomeShowTableViewCell.self, forCellReuseIdentifier: MPPositive_HomeShowTableViewCellID) tableView.register(MPPositive_HomeShowTableViewCell.self, forCellReuseIdentifier: MPPositive_HomeShowTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView return tableView
}() }()
private let MPPositive_HomeShowTableViewCellID = "MPPositive_HomeShowTableViewCell" private let MPPositive_HomeShowTableViewCellID = "MPPositive_HomeShowTableViewCell"
@ -90,7 +91,17 @@ extension MPPositive_HomeViewController: UITableViewDataSource, UITableViewDeleg
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_HomeShowTableViewCellID, for: indexPath) as! MPPositive_HomeShowTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_HomeShowTableViewCellID, for: indexPath) as! MPPositive_HomeShowTableViewCell
cell.browseViewModel = MPPositive_BrowseLoadViewModel.shared.browseModuleLists[indexPath.section] cell.browseViewModel = MPPositive_BrowseLoadViewModel.shared.browseModuleLists[indexPath.section]
cell.showType = .init(rawValue: (indexPath.section > 4 ? 4:indexPath.section))! if MPPositive_BrowseLoadViewModel.shared.browseModuleLists[indexPath.section].items.first?.browseItem.itemType == .single {
//
if MPPositive_BrowseLoadViewModel.shared.browseModuleLists[indexPath.section].items.first?.browseItem.pageType == "MUSIC_VIDEO_TYPE_OMV" {
cell.showType = .Fifth
}else {
cell.showType = .Frist
}
}else {
//使
cell.showType = .Third
}
cell.requestNextBlock = { cell.requestNextBlock = {
[weak self] (item) in [weak self] (item) in
guard let self = self else {return} guard let self = self else {return}
@ -108,7 +119,7 @@ extension MPPositive_HomeViewController: UITableViewDataSource, UITableViewDeleg
} }
case .list: case .list:
// //
let listVC = MPPositive_ListShowViewController(item.browseItem.browseId ?? "", params: item.browseItem.params ?? "") let listVC = MPPositive_ListShowViewController(item.browseItem.browseId ?? "", params: item.browseItem.params ?? "", title: item.title ?? "", subtitle: item.subtitle ?? "")
navigationController?.pushViewController(listVC, animated: true) navigationController?.pushViewController(listVC, animated: true)
default: default:
break break

View File

@ -64,6 +64,7 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.register(MPPositive_MusicItemShowTableViewCell.self, forCellReuseIdentifier: MPPositive_MusicItemShowTableViewCellID) tableView.register(MPPositive_MusicItemShowTableViewCell.self, forCellReuseIdentifier: MPPositive_MusicItemShowTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView return tableView
}() }()
private let MPPositive_MusicItemShowTableViewCellID = "MPPositive_MusicItemShowTableViewCell" private let MPPositive_MusicItemShowTableViewCellID = "MPPositive_MusicItemShowTableViewCell"
@ -81,16 +82,20 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
} }
} }
var browseid:String = "" private var browseid:String!
var params:String = "" private var params:String!
private var centerTtitle:String!
private var subtitle:String!
/// ///
/// - Parameters: /// - Parameters:
/// - browseId: id /// - browseId: id
/// - params: /// - params:
init(_ browseId:String, params:String) { init(_ browseId:String, params:String, title:String, subtitle:String) {
super.init(nibName: nil, bundle: nil)
self.browseid = browseId self.browseid = browseId
self.params = params self.params = params
self.centerTtitle = title
self.subtitle = subtitle
super.init(nibName: nil, bundle: nil)
// //
MP_NetWorkManager.shared.requestAlbumOrListDatas(browseId, params: params) { [weak self] result in MP_NetWorkManager.shared.requestAlbumOrListDatas(browseId, params: params) { [weak self] result in
guard let self = self else {return} guard let self = self else {return}
@ -237,23 +242,24 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
if self.collectionListBtn.isSelected == true{ if self.collectionListBtn.isSelected == true{
self.collectionListBtn.isSelected = false self.collectionListBtn.isSelected = false
MPPositive_CollectionListModel.fetch(.init(format: "browseId == %@", self.browseid)).forEach { i in MPPositive_CollectionListModel.fetch(.init(format: "browseId == %@", self.browseid)).forEach { i in
if i.browseId == self.browseid{ if i.browseId == self.browseid{
if i.params == self.params{ if i.params == self.params{
MPPositive_CollectionListModel.delete(i) MPPositive_CollectionListModel.delete(i)
}
} }
} }
}
MPPositive_LoadCoreModel.shared.reloadCollectionListViewModels(nil)
}else{ }else{
self.collectionListBtn.isSelected = true self.collectionListBtn.isSelected = true
let item = MPPositive_CollectionListModel.create() let item = MPPositive_CollectionListModel.create()
item.title = listOrAlbum.header.title item.title = self.centerTtitle
item.subtitle = self.subtitle
item.browseId = self.browseid item.browseId = self.browseid
item.params = self.params item.params = self.params
item.coverImage = listOrAlbum.header.coverUrl item.coverImage = listOrAlbum.header.coverUrl
MPPositive_CollectionListModel.save() MPPositive_CollectionListModel.save()
MPPositive_LoadCoreModel.shared.reloadCollectionListViewModels(nil)
} }
} }
// //

View File

@ -45,6 +45,7 @@ class MPPositive_MoreContentViewController: MPPositive_BaseViewController {
collectionView.delegate = self collectionView.delegate = self
collectionView.register(MPPositive_MoreListContentCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_MoreListContentCollectionViewCellID) collectionView.register(MPPositive_MoreListContentCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_MoreListContentCollectionViewCellID)
collectionView.register(MPPositive_HomeListFifthCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_HomeListFifthCollectionViewCellID) collectionView.register(MPPositive_HomeListFifthCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_HomeListFifthCollectionViewCellID)
collectionView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return collectionView return collectionView
}() }()
//cell //cell

View File

@ -21,6 +21,7 @@ class MPPositive_PlayerListShowViewController: UIViewController {
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.register(MPPositive_PlayerListShowTableViewCell.self, forCellReuseIdentifier: MPPositive_PlayerListShowTableViewCellID) tableView.register(MPPositive_PlayerListShowTableViewCell.self, forCellReuseIdentifier: MPPositive_PlayerListShowTableViewCellID)
return tableView return tableView
}() }()
private let MPPositive_PlayerListShowTableViewCellID = "MPPositive_PlayerListShowTableViewCell" private let MPPositive_PlayerListShowTableViewCellID = "MPPositive_PlayerListShowTableViewCell"

View File

@ -133,14 +133,11 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
coverView.sliderView.value = Float(value) coverView.sliderView.value = Float(value)
} }
// //
MP_PlayerManager.shared.cacheValueBlock = { [weak self] (value, duration) in MP_PlayerManager.shared.cacheValueBlock = { [weak self] progress in
guard let self = self else { return } guard let self = self else { return }
if value < duration { if progress <= 1 {
// coverView.progressView.setProgress(progress, animated: false)
let float = value/duration
coverView.progressView.setProgress(Float(float), animated: false)
}else { }else {
//
coverView.progressView.setProgress(1, animated: false) coverView.progressView.setProgress(1, animated: false)
} }
} }
@ -165,7 +162,6 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
if MP_PlayerManager.shared.loadPlayer.currentVideo != nil { if MP_PlayerManager.shared.loadPlayer.currentVideo != nil {
uploadUI() uploadUI()
} }
coverView.restoreDownloadProgress()
} }
// //
private func configure() { private func configure() {
@ -311,6 +307,7 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
lyricsView.lyricsLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.lyrics?.isEmpty == true ? "No Lyrics":MP_PlayerManager.shared.loadPlayer.currentVideo?.lyrics lyricsView.lyricsLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.lyrics?.isEmpty == true ? "No Lyrics":MP_PlayerManager.shared.loadPlayer.currentVideo?.lyrics
coverView.loadBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isDlownd ?? false coverView.loadBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isDlownd ?? false
coverView.collectionSongBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isCollection ?? false coverView.collectionSongBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isCollection ?? false
coverView.restoreDownloadProgress()
} }
//MARK: - //MARK: -
// //

View File

@ -51,6 +51,7 @@ class MPPositive_RecommendViewController: MPPositive_BaseViewController {
collectionView.dataSource = self collectionView.dataSource = self
collectionView.delegate = self collectionView.delegate = self
collectionView.register(MPPositive_RecommendMemberCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_RecommendMemberCollectionViewCellID) collectionView.register(MPPositive_RecommendMemberCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_RecommendMemberCollectionViewCellID)
return collectionView return collectionView
}() }()
private let MPPositive_RecommendMemberCollectionViewCellID = "MPPositive_RecommendMemberCollectionViewCell" private let MPPositive_RecommendMemberCollectionViewCellID = "MPPositive_RecommendMemberCollectionViewCell"
@ -190,7 +191,7 @@ extension MPPositive_RecommendViewController: JXSegmentedListContainerViewDataSo
switch item.browseItem.itemType { switch item.browseItem.itemType {
case .list: case .list:
// //
let listVC = MPPositive_ListShowViewController(item.browseItem.browseId ?? "", params: "") let listVC = MPPositive_ListShowViewController(item.browseItem.browseId ?? "", params: "", title: item.title ?? "", subtitle: item.subtitle ?? "")
navigationController?.pushViewController(listVC, animated: true) navigationController?.pushViewController(listVC, animated: true)
case .single: case .single:
/// ///

View File

@ -61,6 +61,21 @@ class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
private lazy var suggestionView:MPPositive_SearchSuggestionsView = .init(frame: .zero) private lazy var suggestionView:MPPositive_SearchSuggestionsView = .init(frame: .zero)
//MARK: - View //MARK: - View
private lazy var resultsShowView:MPPositive_SearchResultsShowView = .init(frame: .zero) private lazy var resultsShowView:MPPositive_SearchResultsShowView = .init(frame: .zero)
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
init(_ text:String) {
super.init(nibName: nil, bundle: nil)
searchTextField.text = text
searchText = text
resultsShowView.loadModel = .init(text)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setTitle("Result") setTitle("Result")
@ -88,7 +103,7 @@ class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
navigationController?.pushViewController(artistVC, animated: true) navigationController?.pushViewController(artistVC, animated: true)
case .list: case .list:
// //
let listVC = MPPositive_ListShowViewController(item.item.browseId ?? "", params: "") let listVC = MPPositive_ListShowViewController(item.item.browseId ?? "", params: "", title: item.title ?? "", subtitle: item.subtitle ?? "")
navigationController?.pushViewController(listVC, animated: true) navigationController?.pushViewController(listVC, animated: true)
case .single: case .single:
/// ///

View File

@ -14,6 +14,28 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
return imageView return imageView
}() }()
private lazy var collectionView:UICollectionView = {
let layout = MPPositive_TagFlowLayout()
layout.delegate = self
let collectionView:UICollectionView = .init(frame: .init(x: 0, y: 0, width: screen_Width, height: screen_Height), collectionViewLayout: layout)
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = .clear
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(MPPositive_SearchTagCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_SearchTagCollectionViewCellID)
return collectionView
}()
private let MPPositive_SearchTagCollectionViewCellID = "MPPositive_SearchTagCollectionViewCell"
//
private lazy var historyLabel:UILabel = createLabel("History", font: .systemFont(ofSize: 14*width, weight: .regular), textColor: .init(hex: "#666666"), textAlignment: .left)
//
private lazy var deleteBtn:UIButton = {
let btn:UIButton = .init()
btn.setBackgroundImage(UIImage(named: "Tag_Delete'logo"), for: .normal)
btn.addTarget(self, action: #selector(deleteClick(_ :)), for: .touchUpInside)
return btn
}()
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setTitle("") setTitle("")
@ -22,6 +44,7 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
collectionView.reloadData()
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
@ -40,6 +63,23 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
make.top.right.left.equalToSuperview() make.top.right.left.equalToSuperview()
make.height.equalTo(981*width) make.height.equalTo(981*width)
} }
view.addSubview(deleteBtn)
deleteBtn.snp.makeConstraints { make in
make.width.height.equalTo(24*width)
make.right.equalToSuperview().offset(-18*width)
make.top.equalTo(navView.snp.bottom).offset(24*width)
}
view.addSubview(historyLabel)
historyLabel.snp.makeConstraints { make in
make.centerY.equalTo(deleteBtn)
make.left.equalToSuperview().offset(18*width)
}
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(deleteBtn.snp.bottom).offset(6*width)
make.height.equalTo(130*width)
}
} }
// //
private func createSearchView() -> UIView{ private func createSearchView() -> UIView{
@ -71,5 +111,124 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
let resultVC = MPPositive_SearchResultShowViewController() let resultVC = MPPositive_SearchResultShowViewController()
navigationController?.pushViewController(resultVC, animated: false) navigationController?.pushViewController(resultVC, animated: false)
} }
//
@objc private func deleteClick(_ sender:UIButton) {
//
MPPositive_SearchTagModel.fetchAll().forEach { item in
MPPositive_SearchTagModel.delete(item)
}
MPPositive_LoadCoreModel.shared.reloadSearchTags {
[weak self] in
guard let self = self else {return}
collectionView.reloadData()
}
}
} }
//MARK: - collectionView
extension MPPositive_SearchViewController: UICollectionViewDataSource, UICollectionViewDelegate, MPPositive_TagLayoutDelegate {
func waterFlowLayout(_ layout: MPPositive_TagFlowLayout, indexPath: IndexPath) -> CGFloat {
let text = MPPositive_LoadCoreModel.shared.searchTags[indexPath.row].text ?? ""
let textWidth = text.textAutoWidth(height: 11.5 * width, font: .systemFont(ofSize: 12 * width, weight: .medium)) + (43 * width)
return textWidth
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return MPPositive_LoadCoreModel.shared.searchTags.count > 10 ? 10:MPPositive_LoadCoreModel.shared.searchTags.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MPPositive_SearchTagCollectionViewCellID, for: indexPath) as! MPPositive_SearchTagCollectionViewCell
cell.setText(MPPositive_LoadCoreModel.shared.searchTags[indexPath.row].text)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let text = MPPositive_LoadCoreModel.shared.searchTags[indexPath.row].text ?? ""
let resultVC = MPPositive_SearchResultShowViewController(text)
navigationController?.pushViewController(resultVC, animated: false)
}
}
//MARK: -
protocol MPPositive_TagLayoutDelegate {
func waterFlowLayout(_ layout:MPPositive_TagFlowLayout,indexPath:IndexPath) -> CGFloat
}
//MARK: -
///layout
class MPPositive_TagFlowLayout: UICollectionViewFlowLayout {
//
var rowHeight:CGFloat = 22 * width
//
var delegate:MPPositive_TagLayoutDelegate?
//x
var originxArray:[CGFloat]!
//y
var originyArray:[CGFloat]!
override init() {
super.init()
//
minimumInteritemSpacing = 8 * width
//
minimumLineSpacing = 8 * width
//
sectionInset = .init(top: 5 * width, left: 18 * width, bottom: 5 * width, right: 18 * width)
scrollDirection = .vertical
//
originxArray = [CGFloat]()
originyArray = [CGFloat]()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: -
//
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func prepare() {
super.prepare()
}
//itemlayoutAttributes
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let array = super.layoutAttributesForElements(in: rect)
var mutArray = [UICollectionViewLayoutAttributes]()
array?.forEach({ (attrs) in
let theAttrs = layoutAttributesForItem(at: attrs.indexPath)
mutArray.append(theAttrs!)
})
return mutArray
}
//itemlayoutAttributes
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
//x
var x = sectionInset.left
var y = sectionInset.top
//cellxy
let preRow = indexPath.row - 1
if preRow >= 0 {
//cell
if originyArray.count > preRow {
//xy
x = originxArray[preRow]
y = originyArray[preRow]
}
let preIndexPath = IndexPath(item: preRow, section: indexPath.section)
//
let preWidth = delegate?.waterFlowLayout(self, indexPath: preIndexPath)
x += preWidth! + minimumInteritemSpacing
}
var currentWidth = delegate?.waterFlowLayout(self, indexPath: indexPath)
//cell
currentWidth = min(currentWidth!, collectionView!.frame.size.width - sectionInset.left - sectionInset.right)
if x + currentWidth! > collectionView!.frame.size.width - sectionInset.right {
//
x = self.sectionInset.left
y += rowHeight + minimumLineSpacing
}
//
let attrs = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
attrs.frame = CGRect(x: x, y: y, width: currentWidth!, height: rowHeight)
originxArray.insert(x, at: indexPath.row)
originyArray.insert(y, at: indexPath.row)
return attrs
}
}

View File

@ -0,0 +1,84 @@
//
// MPPositive_LibraryTableViewCell.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
class MPPositive_LibraryTableViewCell: UITableViewCell {
//
private lazy var iconImageView:UIImageView = {
let imageView:UIImageView = .init()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
return imageView
}()
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 14*width, weight: .medium), textColor: .white, textAlignment: .left)
private lazy var subtitleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.5), textAlignment: .left)
///
private lazy var moreBtn:UIButton = {
let btn:UIButton = .init()
btn.setBackgroundImage(UIImage(named: "Song_More'logo"), for: .normal)
btn.addTarget(self, action: #selector(moreActionClick(_ :)), for: .touchUpInside)
return btn
}()
var listViewModel:MPPositive_CollectionListViewModel!{
didSet{
listViewModel.setImage(iconImageView)
titleLabel.text = listViewModel.title
subtitleLabel.text = listViewModel.subtitle
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
private func configure() {
contentView.addSubview(iconImageView)
iconImageView.snp.makeConstraints { make in
make.width.height.equalTo(50*width)
make.top.equalToSuperview().offset(6*width).priority(999)
make.bottom.equalToSuperview().offset(-6*width)
make.left.equalToSuperview().offset(18*width)
}
contentView.addSubview(moreBtn)
moreBtn.snp.makeConstraints { make in
make.width.height.equalTo(24*width)
make.centerY.equalTo(iconImageView.snp.centerY)
make.right.equalToSuperview().offset(-18*width)
}
contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconImageView.snp.top).offset(10*width)
make.left.equalTo(iconImageView.snp.right).offset(12*width)
make.right.equalTo(moreBtn.snp.left).offset(-10*width)
}
contentView.addSubview(subtitleLabel)
subtitleLabel.snp.makeConstraints { make in
make.bottom.equalTo(iconImageView.snp.bottom).offset(-10*width)
make.left.equalTo(titleLabel.snp.left)
make.right.equalTo(titleLabel.snp.right)
}
}
//
@objc private func moreActionClick(_ sender:UIButton) {
}
}

View File

@ -0,0 +1,60 @@
//
// MPPositive_LoveArtistTableViewCell.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/28.
//
import UIKit
class MPPositive_LoveArtistTableViewCell: UITableViewCell {
//
private lazy var iconImageView:UIImageView = {
let imageView:UIImageView = .init()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
return imageView
}()
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 14*width, weight: .medium), textColor: .white, textAlignment: .left)
var artistViewModel:MPPositive_CollectionArtistViewModel!{
didSet{
artistViewModel.setImage(iconImageView)
titleLabel.text = artistViewModel.title
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
private func configure() {
contentView.addSubview(iconImageView)
iconImageView.snp.makeConstraints { make in
make.width.height.equalTo(50*width)
make.top.equalToSuperview().offset(8*width).priority(999)
make.bottom.equalToSuperview().offset(-8*width)
make.left.equalToSuperview().offset(18*width)
}
contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconImageView.snp.top).offset(10*width)
make.left.equalTo(iconImageView.snp.right).offset(12*width)
make.right.equalToSuperview().offset(-12*width)
}
}
}

View File

@ -102,13 +102,13 @@ class MPPositive_ArtistShowHeaderView: UIView {
// MPPositive_CollectionArtistModel.delete(i) // MPPositive_CollectionArtistModel.delete(i)
// } // }
// } // }
let items = MPPositive_CollectionArtistModel.fetch(.init(format: "artistId == %@", self.artistid)) let items = MPPositive_CollectionArtistModel.fetch(.init(format: "artistId == %@", self.artistid))
for item in items { for item in items {
if item.artistId == self.artistid { if item.artistId == self.artistid {
MPPositive_CollectionArtistModel.delete(item) MPPositive_CollectionArtistModel.delete(item)
} }
} }
MPPositive_LoadCoreModel.shared.reloadCollectionArtistViewModels(nil)
}else{ }else{
self.collectionBtn.isSelected = true self.collectionBtn.isSelected = true
@ -118,7 +118,7 @@ class MPPositive_ArtistShowHeaderView: UIView {
item.subtitle = artist.header.subscriptionedText item.subtitle = artist.header.subscriptionedText
item.artistId = self.artistid item.artistId = self.artistid
MPPositive_CollectionArtistModel.save() MPPositive_CollectionArtistModel.save()
MPPositive_LoadCoreModel.shared.reloadCollectionArtistViewModels(nil)
} }
} }

View File

@ -23,6 +23,7 @@ class MPPositive_ArtistShowTypeView: UIView, JXPagingViewListViewDelegate {
tableView.register(MPPositive_ArtistShowSongTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowSongTableViewCellID) tableView.register(MPPositive_ArtistShowSongTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowSongTableViewCellID)
tableView.register(MPPositive_ArtistShowListableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowListableViewCellID) tableView.register(MPPositive_ArtistShowListableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowListableViewCellID)
tableView.register(MPPositive_ArtistDescriptionTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistDescriptionTableViewCellID) tableView.register(MPPositive_ArtistDescriptionTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistDescriptionTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
// //
let footer = MJRefreshAutoGifFooter { let footer = MJRefreshAutoGifFooter {
[weak self] in [weak self] in

View File

@ -28,13 +28,13 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell {
return btn return btn
}() }()
/// ///
private lazy var loadBtn:UIButton = { // private lazy var loadBtn:UIButton = {
let btn:UIButton = .init() // let btn:UIButton = .init()
btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal) // btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected) // btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside) // btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
return btn // return btn
}() // }()
// //
var itemView:MPPositive_BrowseItemViewModel!{ var itemView:MPPositive_BrowseItemViewModel!{
didSet{ didSet{
@ -81,17 +81,17 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell {
make.centerY.equalTo(coverImageView.snp.centerY) make.centerY.equalTo(coverImageView.snp.centerY)
make.right.equalToSuperview().offset(-18*width) make.right.equalToSuperview().offset(-18*width)
} }
contentView.addSubview(loadBtn) // contentView.addSubview(loadBtn)
loadBtn.snp.makeConstraints { make in // loadBtn.snp.makeConstraints { make in
make.width.height.equalTo(24*width) // make.width.height.equalTo(24*width)
make.centerY.equalTo(coverImageView.snp.centerY) // make.centerY.equalTo(coverImageView.snp.centerY)
make.right.equalToSuperview().offset(-54*width) // make.right.equalToSuperview().offset(-54*width)
} // }
contentView.addSubview(titleLabel) contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in titleLabel.snp.makeConstraints { make in
make.top.equalTo(coverImageView.snp.top).offset(10*width) make.top.equalTo(coverImageView.snp.top).offset(10*width)
make.left.equalTo(coverImageView.snp.right).offset(12*width) make.left.equalTo(coverImageView.snp.right).offset(12*width)
make.right.equalTo(loadBtn.snp.left).offset(-12*width) make.right.equalTo(moreBtn.snp.left).offset(-12*width)
} }
contentView.addSubview(subtitleLabel) contentView.addSubview(subtitleLabel)
subtitleLabel.snp.makeConstraints { make in subtitleLabel.snp.makeConstraints { make in

View File

@ -51,6 +51,7 @@ class MPPositive_PlayerCoverView: UIView {
progressView.isUserInteractionEnabled = true progressView.isUserInteractionEnabled = true
progressView.progressTintColor = .init(hex: "#FFFFFF", alpha: 0.3) progressView.progressTintColor = .init(hex: "#FFFFFF", alpha: 0.3)
progressView.trackTintColor = .clear progressView.trackTintColor = .clear
progressView.progress = 0
return progressView return progressView
}() }()
///Label ///Label
@ -72,24 +73,15 @@ class MPPositive_PlayerCoverView: UIView {
} }
return maskView return maskView
}() }()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
backgroundColor = .clear backgroundColor = .clear
configure() configure()
// //
NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkNotReachableAction(_:)), notificationName: .net_switch_notReachable) NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkNotReachableAction(_:)), notificationName: .net_switch_notReachable)
NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkReachableAction(_:)), notificationName: .net_switch_reachable) NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkReachableAction(_:)), notificationName: .net_switch_reachable)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
// NotificationCenter.default.addObserver(self, selector: #selector(updateProgress(_:)), name: Notification.Name("DownloadProgressUpdated"), object: nil) // NotificationCenter.default.addObserver(self, selector: #selector(updateProgress(_:)), name: Notification.Name("DownloadProgressUpdated"), object: nil)
@ -100,20 +92,26 @@ class MPPositive_PlayerCoverView: UIView {
public func restoreDownloadProgress() { public func restoreDownloadProgress() {
if let currentVideo = MP_PlayerManager.shared.loadPlayer.currentVideo, if let currentVideo = MP_PlayerManager.shared.loadPlayer.currentVideo,
let videoURLString = currentVideo.song.resourceUrls?.first, let videoURLString = currentVideo.song.resourceUrls?.first,
let videoURL = URL(string: videoURLString) { let videoURL = URL(string: videoURLString) {
if let progress = DownloadManager.shared.getProgress(for: videoURL) { if let progress = DownloadManager.shared.getProgress(for: videoURL) {
//VideoID
addCircularProgressBar(over: loadBtn) addCircularProgressBar(over: loadBtn)
loadView.setProgress(to: progress) loadView.setProgress(to: progress)
// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // 0.1
self.layoutIfNeeded() self.layoutIfNeeded()
self.loadBtn.setBackgroundImage(UIImage(named: ""), for: .normal) self.loadBtn.setBackgroundImage(UIImage(named: ""), for: .normal)
self.loadBtn.setImage(UIImage(named: "download"), for: .normal) self.loadBtn.setImage(UIImage(named: "download"), for: .normal)
self.addCircularProgressBar(over: self.loadBtn) self.addCircularProgressBar(over: self.loadBtn)
self.loadView.setProgress(to: progress) self.loadView.setProgress(to: progress)
// } }else {
//videoID
if (loadView.superview) != nil {
loadView.removeFromSuperview()
}
self.loadBtn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
self.loadBtn.setImage(UIImage(), for: .normal)
self.loadBtn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
} }
} }
} }
@ -135,10 +133,6 @@ class MPPositive_PlayerCoverView: UIView {
} }
} }
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
@ -257,6 +251,7 @@ class MPPositive_PlayerCoverView: UIView {
} }
} }
MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad() MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad()
MPPositive_LoadCoreModel.shared.reloadCollectionSongViewModel(nil)
} }
}else{ }else{
self.collectionSongBtn.isSelected = true self.collectionSongBtn.isSelected = true
@ -266,8 +261,11 @@ class MPPositive_PlayerCoverView: UIView {
item.videoId = MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId item.videoId = MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId
item.subtitle = MP_PlayerManager.shared.loadPlayer.currentVideo.subtitle item.subtitle = MP_PlayerManager.shared.loadPlayer.currentVideo.subtitle
item.coverImage = MP_PlayerManager.shared.loadPlayer.currentVideo.coverUrl item.coverImage = MP_PlayerManager.shared.loadPlayer.currentVideo.coverUrl
item.lyricsID = MP_PlayerManager.shared.loadPlayer.currentVideo.song.lyricsID
item.relatedID = MP_PlayerManager.shared.loadPlayer.currentVideo.song.relatedID
MPPositive_CollectionSongModel.save() MPPositive_CollectionSongModel.save()
MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad() MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad()
MPPositive_LoadCoreModel.shared.reloadCollectionSongViewModel(nil)
} }
} }
} }
@ -281,26 +279,26 @@ class MPPositive_PlayerCoverView: UIView {
if let currentVideo = MP_PlayerManager.shared.loadPlayer.currentVideo, if let currentVideo = MP_PlayerManager.shared.loadPlayer.currentVideo,
let videoURLString = currentVideo.song.resourceUrls?.first, let videoURLString = currentVideo.song.resourceUrls?.first,
let videoURL = URL(string: videoURLString) { let videoURL = URL(string: videoURLString) {
let videoId = currentVideo.song.videoId ?? "default_video_id" DownloadManager.shared.downloadVideo(from: videoURL, song: MP_PlayerManager.shared.loadPlayer.currentVideo.song, progressHandler: { [weak self] progress in
DownloadManager.shared.downloadVideo(from: videoURL, videoId: videoId, progressHandler: { [weak self] progress in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.loadView.setProgress(to: progress) self?.loadView.setProgress(to: progress)
NotificationCenter.default.post(name: Notification.Name("DownloadProgressUpdated"), object: nil, userInfo: ["url": videoURL, "progress": progress]) NotificationCenter.default.post(name: Notification.Name("DownloadProgressUpdated"), object: nil, userInfo: ["url": videoURL, "progress": progress])
} }
}, completion: { [weak self] result in }, completion: { [weak self] result in
switch result { switch result {
case .success(let fileURL): case .success(let song):
let item = MPPositive_DownloadItemModel.create() let item = MPPositive_DownloadItemModel.create()
item.resourcePath = "\(fileURL)" //
item.coverImage = URL(string: MP_PlayerManager.shared.loadPlayer.currentVideo.song.coverUrls!.first!) item.coverImage = song.coverUrls!.last
item.reviewImage = URL(string: MP_PlayerManager.shared.loadPlayer.currentVideo.song.reviewUrls!.first!) item.reviewImage = song.reviewUrls!.last
item.title = MP_PlayerManager.shared.loadPlayer.currentVideo.song.title item.title = song.title
item.longBylineText = MP_PlayerManager.shared.loadPlayer.currentVideo.song.longBylineText item.longBylineText = song.longBylineText
item.lengthText = MP_PlayerManager.shared.loadPlayer.currentVideo.song.lengthText item.lengthText = song.lengthText
item.shortBylineText = MP_PlayerManager.shared.loadPlayer.currentVideo.song.shortBylineText item.shortBylineText = song.shortBylineText
item.lyrics = MP_PlayerManager.shared.loadPlayer.currentVideo.lyrics item.lyrics = song.lyrics
item.videoId = MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId item.lyricsID = song.lyricsID
item.relatedID = MP_PlayerManager.shared.loadPlayer.currentVideo.song.relatedID item.videoId = song.videoId
item.relatedID = song.relatedID
MPPositive_DownloadItemModel.save() MPPositive_DownloadItemModel.save()
DispatchQueue.main.async { DispatchQueue.main.async {
@ -308,6 +306,8 @@ class MPPositive_PlayerCoverView: UIView {
MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad() MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad()
self?.loadBtn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .normal) self?.loadBtn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .normal)
self?.loadBtn.setImage(UIImage(named: ""), for: .normal) self?.loadBtn.setImage(UIImage(named: ""), for: .normal)
print("完成了对\(song.title ?? "")的下载")
MPPositive_LoadCoreModel.shared.reloadLoadSongViewModel(nil)
} }
case .failure(let error): case .failure(let error):
print("Download failed with error: \(error)") print("Download failed with error: \(error)")

View File

@ -22,6 +22,7 @@ class MPPositive_RecommendShowTypeView: UIView, JXSegmentedListContainerViewList
tableView.delegate = self tableView.delegate = self
tableView.register(MPPositive_ArtistShowSongTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowSongTableViewCellID) tableView.register(MPPositive_ArtistShowSongTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowSongTableViewCellID)
tableView.register(MPPositive_ArtistShowListableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowListableViewCellID) tableView.register(MPPositive_ArtistShowListableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowListableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView return tableView
}() }()
private let MPPositive_ArtistShowSongTableViewCellID = "MPPositive_ArtistShowSongTableViewCell" private let MPPositive_ArtistShowSongTableViewCellID = "MPPositive_ArtistShowSongTableViewCell"

View File

@ -21,6 +21,7 @@ class MPPositive_SearchResultPreviewShowView: UIView, JXSegmentedListContainerVi
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID) tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
return tableView return tableView
}() }()
private let MPPositive_SearchResultShowTableViewCellID = "MPPositive_SearchResultShowTableViewCell" private let MPPositive_SearchResultShowTableViewCellID = "MPPositive_SearchResultShowTableViewCell"

View File

@ -24,14 +24,14 @@ class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
btn.addTarget(self, action: #selector(moreActionClick(_ :)), for: .touchUpInside) btn.addTarget(self, action: #selector(moreActionClick(_ :)), for: .touchUpInside)
return btn return btn
}() }()
/// // ///
private lazy var loadBtn:UIButton = { // private lazy var loadBtn:UIButton = {
let btn:UIButton = .init() // let btn:UIButton = .init()
btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal) // btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected) // btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside) // btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
return btn // return btn
}() // }()
var itemView:MPPositive_SearchResultItemViewModel!{ var itemView:MPPositive_SearchResultItemViewModel!{
didSet{ didSet{
itemView.setImage(iconImageView) itemView.setImage(iconImageView)
@ -40,13 +40,28 @@ class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
// //
if itemView.item.itemType == .single { if itemView.item.itemType == .single {
moreBtn.isHidden = false moreBtn.isHidden = false
loadBtn.isHidden = false // loadBtn.isHidden = false
}else { }else {
moreBtn.isHidden = true moreBtn.isHidden = true
loadBtn.isHidden = true // loadBtn.isHidden = true
} }
} }
} }
var songViewModel:MPPositive_CollectionSongViewModel!{
didSet{
songViewModel.setImage(iconImageView)
titleLabel.text = songViewModel.title
subtitleLabel.text = songViewModel.subtitle
}
}
var loadViewModel:MPPositive_DownloadViewModel!{
didSet{
loadViewModel.setImage(iconImageView)
titleLabel.text = loadViewModel.title
subtitleLabel.text = loadViewModel.subtitle
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none selectionStyle = .none
@ -81,17 +96,17 @@ class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
make.centerY.equalTo(iconImageView.snp.centerY) make.centerY.equalTo(iconImageView.snp.centerY)
make.right.equalToSuperview().offset(-18*width) make.right.equalToSuperview().offset(-18*width)
} }
contentView.addSubview(loadBtn) // contentView.addSubview(loadBtn)
loadBtn.snp.makeConstraints { make in // loadBtn.snp.makeConstraints { make in
make.width.height.equalTo(24*width) // make.width.height.equalTo(24*width)
make.centerY.equalTo(iconImageView.snp.centerY) // make.centerY.equalTo(iconImageView.snp.centerY)
make.right.equalToSuperview().offset(-54*width) // make.right.equalToSuperview().offset(-54*width)
} // }
contentView.addSubview(titleLabel) contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconImageView.snp.top).offset(10*width) make.top.equalTo(iconImageView.snp.top).offset(10*width)
make.left.equalTo(iconImageView.snp.right).offset(12*width) make.left.equalTo(iconImageView.snp.right).offset(12*width)
make.right.equalTo(loadBtn.snp.left).offset(-10*width) make.right.equalTo(moreBtn.snp.left).offset(-10*width)
} }
contentView.addSubview(subtitleLabel) contentView.addSubview(subtitleLabel)
subtitleLabel.snp.makeConstraints { make in subtitleLabel.snp.makeConstraints { make in

View File

@ -22,6 +22,7 @@ class MPPositive_SearchResultTypeShowView: UIView, JXSegmentedListContainerViewL
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID) tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID)
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
// //
let footer = MJRefreshAutoGifFooter { let footer = MJRefreshAutoGifFooter {
[weak self] in [weak self] in

View File

@ -0,0 +1,35 @@
//
// MPPositive_SearchTagCollectionViewCell.swift
// MusicPlayer
//
// Created by Mr.Zhou on 2024/5/29.
//
import UIKit
class MPPositive_SearchTagCollectionViewCell: UICollectionViewCell {
//label
private lazy var titleLabel:UILabel = createLabel("Title", font: .systemFont(ofSize: 12*width, weight: .medium), textColor: .white, textAlignment: .center)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .init(hex: "#1F1F1F")
layer.masksToBounds = true
layer.cornerRadius = 10*width
confirgue()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
//
private func confirgue() {
addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(12*width)
}
}
func setText(_ text:String) {
titleLabel.text = text
}
}

View File

@ -23,5 +23,8 @@ pod 'JXSegmentedView'
pod 'JXPagingView/Paging' pod 'JXPagingView/Paging'
#刷新支持 #刷新支持
pod 'MJRefresh' pod 'MJRefresh'
#流音频播放
pod 'FreeStreamer'
#下载框架
pod 'Tiercel'
end end

View File

@ -1,18 +1,23 @@
PODS: PODS:
- Alamofire (5.9.1) - Alamofire (5.9.1)
- FreeStreamer (4.0.0):
- Reachability (~> 3.0)
- IQKeyboardManagerSwift (6.5.16) - IQKeyboardManagerSwift (6.5.16)
- JXPagingView/Paging (2.1.3) - JXPagingView/Paging (2.1.3)
- JXSegmentedView (1.3.3) - JXSegmentedView (1.3.3)
- Kingfisher (7.11.0) - Kingfisher (7.11.0)
- MJRefresh (3.7.9) - MJRefresh (3.7.9)
- Reachability (3.7.6)
- SnapKit (5.7.1) - SnapKit (5.7.1)
- SVProgressHUD (2.3.1): - SVProgressHUD (2.3.1):
- SVProgressHUD/Core (= 2.3.1) - SVProgressHUD/Core (= 2.3.1)
- SVProgressHUD/Core (2.3.1) - SVProgressHUD/Core (2.3.1)
- SwiftDate (6.3.1) - SwiftDate (6.3.1)
- Tiercel (3.2.5)
DEPENDENCIES: DEPENDENCIES:
- Alamofire - Alamofire
- FreeStreamer
- IQKeyboardManagerSwift - IQKeyboardManagerSwift
- JXPagingView/Paging - JXPagingView/Paging
- JXSegmentedView - JXSegmentedView
@ -21,30 +26,37 @@ DEPENDENCIES:
- SnapKit - SnapKit
- SVProgressHUD - SVProgressHUD
- SwiftDate - SwiftDate
- Tiercel
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- Alamofire - Alamofire
- FreeStreamer
- IQKeyboardManagerSwift - IQKeyboardManagerSwift
- JXPagingView - JXPagingView
- JXSegmentedView - JXSegmentedView
- Kingfisher - Kingfisher
- MJRefresh - MJRefresh
- Reachability
- SnapKit - SnapKit
- SVProgressHUD - SVProgressHUD
- SwiftDate - SwiftDate
- Tiercel
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
FreeStreamer: 7e9c976045701ac2f7e9c14c17245203c37bf2ea
IQKeyboardManagerSwift: 12d89768845bb77b55cc092ecc2b1f9370f06b76 IQKeyboardManagerSwift: 12d89768845bb77b55cc092ecc2b1f9370f06b76
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68 JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78 MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
Reachability: fd0ecd23705e2599e4cceeb943222ae02296cbc6
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22 SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2 SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
Tiercel: c0a73f876a72800333b15f4e7e48791f4ad21e90
PODFILE CHECKSUM: ba88795291c32ea83d380e5384537ca7f5568cd7 PODFILE CHECKSUM: 3804949e23587f6d341ef21aa5e0b1c55a818968
COCOAPODS: 1.15.2 COCOAPODS: 1.15.2

View File

@ -0,0 +1,274 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <Foundation/Foundation.h>
#include "FSAudioStream.h"
@class FSCheckContentTypeRequest;
@class FSParsePlaylistRequest;
@class FSParseRssPodcastFeedRequest;
@class FSPlaylistItem;
@protocol FSAudioControllerDelegate;
/**
* FSAudioController is functionally equivalent to FSAudioStream with
* one addition: it can be directly fed with a playlist (PLS, M3U) URL
* or an RSS podcast feed. It determines the content type and forms
* a playlist for playback. Notice that this generates more traffic and
* is generally more slower than using an FSAudioStream directly.
*
* It is also possible to construct a playlist by yourself by providing
* the playlist items. In this case see the methods for managing the playlist.
*
* If you have a playlist with multiple items, FSAudioController attemps
* automatically preload the next item in the playlist. This helps to
* start the next item playback immediately without the need for the
* user to wait for buffering.
*
* Notice that do not attempt to set your own blocks to the audio stream
* owned by the controller. FSAudioController uses the blocks internally
* and any user set blocks will be overwritten. Instead use the blocks
* offered by FSAudioController.
*/
@interface FSAudioController : NSObject {
NSURL *_url;
NSMutableArray *_streams;
float _volume;
BOOL _readyToPlay;
FSCheckContentTypeRequest *_checkContentTypeRequest;
FSParsePlaylistRequest *_parsePlaylistRequest;
FSParseRssPodcastFeedRequest *_parseRssPodcastFeedRequest;
void (^_onStateChangeBlock)(FSAudioStreamState);
void (^_onMetaDataAvailableBlock)(NSDictionary*);
void (^_onFailureBlock)(FSAudioStreamError error, NSString *errorDescription);
}
/**
* Initializes the audio stream with an URL.
*
* @param url The URL from which the stream data is retrieved.
*/
- (id)initWithUrl:(NSURL *)url;
/**
* Starts playing the stream. Before the playback starts,
* the URL content type is checked and playlists resolved.
*/
- (void)play;
/**
* Starts playing the stream from an URL. Before the playback starts,
* the URL content type is checked and playlists resolved.
*
* @param url The URL from which the stream data is retrieved.
*/
- (void)playFromURL:(NSURL *)url;
/**
* Starts playing the stream from the given playlist. Each item in the array
* must an FSPlaylistItem.
*
* @param playlist The playlist items.
*/
- (void)playFromPlaylist:(NSArray *)playlist;
/**
* Starts playing the stream from the given playlist. Each item in the array
* must an FSPlaylistItem. The playback starts from the given index
* in the playlist.
*
* @param playlist The playlist items.
* @param index The playlist index where to start playback from.
*/
- (void)playFromPlaylist:(NSArray *)playlist itemIndex:(NSUInteger)index;
/**
* Plays a playlist item at the specified index.
*
* @param index The playlist index where to start playback from.
*/
- (void)playItemAtIndex:(NSUInteger)index;
/**
* Returns the count of playlist items.
*/
- (NSUInteger)countOfItems;
/**
* Adds an item to the playlist.
*
* @param item The playlist item to be added.
*/
- (void)addItem:(FSPlaylistItem *)item;
/**
* Adds an item to the playlist at a specific position.
*
* @param item The playlist item to be added.
* @param index The location in the playlist to place the new item
*/
- (void)insertItem:(FSPlaylistItem *)item atIndex:(NSInteger)index;
/**
* Moves an item already in the playlist to a different position in the playlist
*
* @param from The original index of the track to move
* @param to The destination of the the track at the index specified in `from`
*/
- (void)moveItemAtIndex:(NSUInteger)from toIndex:(NSUInteger)to;
/**
* Replaces a playlist item.
*
* @param index The index of the playlist item to be replaced.
* @param item The playlist item used the replace the existing one.
*/
- (void)replaceItemAtIndex:(NSUInteger)index withItem:(FSPlaylistItem *)item;
/**
* Removes a playlist item.
*
* @param index The index of the playlist item to be removed.
*/
- (void)removeItemAtIndex:(NSUInteger)index;
/**
* Stops the stream playback.
*/
- (void)stop;
/**
* If the stream is playing, the stream playback is paused upon calling pause.
* Otherwise (the stream is paused), calling pause will continue the playback.
*/
- (void)pause;
/**
* Returns the playback status: YES if the stream is playing, NO otherwise.
*/
- (BOOL)isPlaying;
/**
* Returns if the current multiple-item playlist has next item
*/
- (BOOL)hasNextItem;
/**
* Returns if the current multiple-item playlist has Previous item
*/
- (BOOL)hasPreviousItem;
/**
* Play the next item of multiple-item playlist
*/
- (void)playNextItem;
/**
* Play the previous item of multiple-item playlist
*/
- (void)playPreviousItem;
/**
* This property holds the current playback volume of the stream,
* from 0.0 to 1.0.
*
* Note that the overall volume is still constrained by the volume
* set by the user! So the actual volume cannot be higher
* than the volume currently set by the user. For example, if
* requesting a volume of 0.5, then the volume will be 50%
* lower than the current playback volume set by the user.
*/
@property (nonatomic,assign) float volume;
/**
* The controller URL.
*/
@property (nonatomic,assign) NSURL *url;
/**
* The the active playing stream, which may change
* from time to time during the playback. In this way, do not
* set your own blocks to the stream but use the blocks
* provides by FSAudioController.
*/
@property (readonly) FSAudioStream *activeStream;
/**
* The playlist item the controller is currently using.
*/
@property (nonatomic,readonly) FSPlaylistItem *currentPlaylistItem;
/**
* This property determines if the next playlist item should be loaded
* automatically. This is YES by default.
*/
@property (nonatomic,assign) BOOL preloadNextPlaylistItemAutomatically;
/**
* This property determines if the debug output is enabled. Disabled
* by default
*/
@property (nonatomic,assign) BOOL enableDebugOutput;
/**
* This property determines if automatic audio session handling is enabled.
* This is YES by default.
*/
@property (nonatomic,assign) BOOL automaticAudioSessionHandlingEnabled;
/**
* This property holds the configuration used for the streaming.
*/
@property (nonatomic,strong) FSStreamConfiguration *configuration;
/**
* Called upon a state change.
*/
@property (copy) void (^onStateChange)(FSAudioStreamState state);
/**
* Called upon a meta data is available.
*/
@property (copy) void (^onMetaDataAvailable)(NSDictionary *metadata);
/**
* Called upon a failure.
*/
@property (copy) void (^onFailure)(FSAudioStreamError error, NSString *errorDescription);
/**
* Delegate.
*/
@property (nonatomic,unsafe_unretained) IBOutlet id<FSAudioControllerDelegate> delegate;
@end
/**
* To check the preloading status, use this delegate.
*/
@protocol FSAudioControllerDelegate <NSObject>
@optional
/**
* Called when the controller wants to start preloading an item. Return YES or NO
* depending if you want this item to be preloaded.
*
* @param audioController The audio controller which is doing the preloading.
* @param stream The stream which is wanted to be preloaded.
*/
- (BOOL)audioController:(FSAudioController *)audioController allowPreloadingForStream:(FSAudioStream *)stream;
/**
* Called when the controller starts to preload an item.
*
* @param audioController The audio controller which is doing the preloading.
* @param stream The stream which is preloaded.
*/
- (void)audioController:(FSAudioController *)audioController preloadStartedForStream:(FSAudioStream *)stream;
@end

View File

@ -0,0 +1,875 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import "FSAudioController.h"
#import "FSPlaylistItem.h"
#import "FSCheckContentTypeRequest.h"
#import "FSParsePlaylistRequest.h"
#import "FSParseRssPodcastFeedRequest.h"
#import <AVFoundation/AVFoundation.h>
/**
* Private interface for FSAudioController.
*/
@interface FSAudioController ()
- (void)notifyRetrievingURL;
@property (readonly) FSAudioStream *audioStream;
@property (readonly) FSCheckContentTypeRequest *checkContentTypeRequest;
@property (readonly) FSParsePlaylistRequest *parsePlaylistRequest;
@property (readonly) FSParseRssPodcastFeedRequest *parseRssPodcastFeedRequest;
@property (nonatomic,assign) BOOL readyToPlay;
@property (nonatomic,assign) NSUInteger currentPlaylistItemIndex;
@property (nonatomic,strong) NSMutableArray *playlistItems;
@property (nonatomic,strong) NSMutableArray *streams;
@property (nonatomic,assign) BOOL needToSetVolume;
@property (nonatomic,assign) BOOL songSwitchInProgress;
@property (nonatomic,assign) float outputVolume;
- (void)audioStreamStateDidChange:(NSNotification *)notification;
- (void)deactivateInactivateStreams:(NSUInteger)currentActiveStream;
- (void)setAudioSessionActive:(BOOL)active;
@end
/**
* Acts as a proxy object for FSAudioStream. Lazily initializes
* the stream when it is needed.
*
* A call to deactivate releases the stream.
*/
@interface FSAudioStreamProxy : NSObject {
FSAudioStream *_audioStream;
}
@property (readonly) FSAudioStream *audioStream;
@property (nonatomic,copy) NSURL *url;
@property (nonatomic,weak) FSAudioController *audioController;
- (void)deactivate;
@end
/*
* =======================================
* FSAudioStreamProxy implementation.
* =======================================
*/
@implementation FSAudioStreamProxy
- (id)init
{
if (self = [super init]) {
}
return self;
}
- (id)initWithAudioController:(FSAudioController *)controller
{
if (self = [self init]) {
self.audioController = controller;
}
return self;
}
- (void)dealloc
{
if (self.audioController.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] FSAudioStreamProxy.dealloc: %@", __LINE__, self.url);
}
[self deactivate];
}
- (FSAudioStream *)audioStream
{
if (!_audioStream) {
FSStreamConfiguration *conf;
if (self.audioController.configuration) {
conf = self.audioController.configuration;
} else {
conf = [[FSStreamConfiguration alloc] init];
}
// Disable audio session handling for the audio stream; audio controller handles it
conf.automaticAudioSessionHandlingEnabled = NO;
_audioStream = [[FSAudioStream alloc] initWithConfiguration:conf];
if (self.audioController.needToSetVolume) {
_audioStream.volume = self.audioController.outputVolume;
}
if (self.url) {
_audioStream.url = self.url;
}
}
return _audioStream;
}
- (void)deactivate
{
[_audioStream stop];
_audioStream = nil;
}
@end
/*
* =======================================
* FSAudioController implementation
* =======================================
*/
@implementation FSAudioController
-(id)init
{
if (self = [super init]) {
_url = nil;
_checkContentTypeRequest = nil;
_parsePlaylistRequest = nil;
_readyToPlay = NO;
_playlistItems = [[NSMutableArray alloc] init];
_streams = [[NSMutableArray alloc] init];
self.preloadNextPlaylistItemAutomatically = YES;
self.enableDebugOutput = NO;
self.automaticAudioSessionHandlingEnabled = YES;
self.configuration = [[FSStreamConfiguration alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(audioStreamStateDidChange:)
name:FSAudioStreamStateChangeNotification
object:nil];
}
return self;
}
- (id)initWithUrl:(NSURL *)url
{
if (self = [self init]) {
self.url = url;
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_checkContentTypeRequest cancel];
[_parsePlaylistRequest cancel];
[_parseRssPodcastFeedRequest cancel];
for (FSAudioStreamProxy *proxy in _streams) {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] dealloc. Deactivating stream %@", __LINE__, proxy.url);
}
[proxy deactivate];
}
[self setAudioSessionActive:NO];
}
- (void)audioStreamStateDidChange:(NSNotification *)notification
{
if (notification.object == self) {
// URL retrieving notification from ourselves, ignore
return;
}
if (!(notification.object == self.audioStream)) {
// This doesn't concern us, return
return;
}
NSDictionary *dict = [notification userInfo];
int state = [[dict valueForKey:FSAudioStreamNotificationKey_State] intValue];
if (state == kFSAudioStreamEndOfFile) {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] EOF reached for %@", __LINE__, self.audioStream.url);
}
if (!self.preloadNextPlaylistItemAutomatically) {
// No preloading wanted, skip
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] Preloading disabled, return.", __LINE__);
}
return;
}
// Reached EOF for this stream, do we have another item waiting in the playlist?
if ([self hasNextItem]) {
FSAudioStreamProxy *proxy = [_streams objectAtIndex:self.currentPlaylistItemIndex + 1];
FSAudioStream *nextStream = proxy.audioStream;
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] Preloading %@", __LINE__, nextStream.url);
}
if ([self.delegate respondsToSelector:@selector(audioController:allowPreloadingForStream:)]) {
if ([self.delegate audioController:self allowPreloadingForStream:nextStream]) {
[nextStream preload];
} else {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] Preloading disallowed for stream %@", __LINE__, nextStream.url);
}
}
} else {
// Start preloading the next stream; we can load this as there is no override
[nextStream preload];
}
if ([self.delegate respondsToSelector:@selector(audioController:preloadStartedForStream:)]) {
[self.delegate audioController:self preloadStartedForStream:nextStream];
}
}
} else if (state == kFsAudioStreamStopped && !self.songSwitchInProgress) {
if (self.enableDebugOutput) {
NSLog(@"Stream %@ stopped. No next playlist items. Deactivating audio session", self.audioStream.url);
}
[self setAudioSessionActive:NO];
} else if (state == kFsAudioStreamPlaybackCompleted && [self hasNextItem]) {
self.currentPlaylistItemIndex = self.currentPlaylistItemIndex + 1;
self.songSwitchInProgress = YES;
[self play];
} else if (state == kFsAudioStreamFailed) {
if (self.enableDebugOutput) {
NSLog(@"Stream %@ failed. Deactivating audio session", self.audioStream.url);
}
[self setAudioSessionActive:NO];
} else if (state == kFsAudioStreamBuffering) {
if (self.enableDebugOutput) {
NSLog(@"Stream buffering. Activating audio session");
}
self.songSwitchInProgress = NO;
if (self.automaticAudioSessionHandlingEnabled) {
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
#endif
}
[self setAudioSessionActive:YES];
} else if (state == kFsAudioStreamPlaying) {
self.currentPlaylistItem.audioDataByteCount = self.activeStream.audioDataByteCount;
}
}
- (void)deactivateInactivateStreams:(NSUInteger)currentActiveStream
{
NSUInteger streamIndex = 0;
for (FSAudioStreamProxy *proxy in _streams) {
if (streamIndex != currentActiveStream) {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] Deactivating stream %@", __LINE__, proxy.url);
}
[proxy deactivate];
}
streamIndex++;
}
}
- (void)setAudioSessionActive:(BOOL)active
{
if (self.automaticAudioSessionHandlingEnabled) {
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
[[AVAudioSession sharedInstance] setActive:active withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
#else
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
[[AVAudioSession sharedInstance] setActive:active error:nil];
#endif
#endif
}
}
/*
* =======================================
* Properties
* =======================================
*/
- (FSAudioStream *)audioStream
{
FSAudioStream *stream = nil;
if ([_streams count] == 0) {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] Stream count %lu, creating a proxy object", __LINE__, (unsigned long)[_streams count]);
}
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
[_streams addObject:proxy];
}
FSAudioStreamProxy *proxy = [_streams objectAtIndex:self.currentPlaylistItemIndex];
stream = proxy.audioStream;
return stream;
}
- (FSCheckContentTypeRequest *)checkContentTypeRequest
{
if (!_checkContentTypeRequest) {
__weak FSAudioController *weakSelf = self;
_checkContentTypeRequest = [[FSCheckContentTypeRequest alloc] init];
_checkContentTypeRequest.url = self.url;
_checkContentTypeRequest.onCompletion = ^() {
if (weakSelf.checkContentTypeRequest.playlist) {
// The URL is a playlist; retrieve the contents
[weakSelf.parsePlaylistRequest start];
} else if (weakSelf.checkContentTypeRequest.xml) {
// The URL may be an RSS feed, check the contents
[weakSelf.parseRssPodcastFeedRequest start];
} else {
// Not a playlist; try directly playing the URL
weakSelf.readyToPlay = YES;
[weakSelf play];
}
};
_checkContentTypeRequest.onFailure = ^() {
// Failed to check the format; try playing anyway
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSAudioController: Failed to check the format, trying to play anyway, URL: %@", weakSelf.audioStream.url);
#endif
weakSelf.readyToPlay = YES;
[weakSelf play];
};
}
return _checkContentTypeRequest;
}
- (FSParsePlaylistRequest *)parsePlaylistRequest
{
if (!_parsePlaylistRequest) {
__weak FSAudioController *weakSelf = self;
_parsePlaylistRequest = [[FSParsePlaylistRequest alloc] init];
_parsePlaylistRequest.onCompletion = ^() {
[weakSelf playFromPlaylist:weakSelf.parsePlaylistRequest.playlistItems];
};
_parsePlaylistRequest.onFailure = ^() {
// Failed to parse the playlist; try playing anyway
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSAudioController: Playlist parsing failed, trying to play anyway, URL: %@", weakSelf.audioStream.url);
#endif
weakSelf.readyToPlay = YES;
[weakSelf play];
};
}
return _parsePlaylistRequest;
}
- (FSParseRssPodcastFeedRequest *)parseRssPodcastFeedRequest
{
if (!_parseRssPodcastFeedRequest) {
__weak FSAudioController *weakSelf = self;
_parseRssPodcastFeedRequest = [[FSParseRssPodcastFeedRequest alloc] init];
_parseRssPodcastFeedRequest.onCompletion = ^() {
[weakSelf playFromPlaylist:weakSelf.parseRssPodcastFeedRequest.playlistItems];
};
_parseRssPodcastFeedRequest.onFailure = ^() {
// Failed to parse the XML file; try playing anyway
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSAudioController: Failed to parse the RSS feed, trying to play anyway, URL: %@", weakSelf.audioStream.url);
#endif
weakSelf.readyToPlay = YES;
[weakSelf play];
};
}
return _parseRssPodcastFeedRequest;
}
- (void)notifyRetrievingURL
{
if (self.onStateChange) {
self.onStateChange(kFsAudioStreamRetrievingURL);
}
}
- (BOOL)isPlaying
{
return [self.audioStream isPlaying];
}
/*
* =======================================
* Public interface
* =======================================
*/
- (void)play
{
if (!self.readyToPlay) {
/*
* Not ready to play; start by checking the content type of the given
* URL.
*/
[self.checkContentTypeRequest start];
NSDictionary *userInfo = @{FSAudioStreamNotificationKey_State: @(kFsAudioStreamRetrievingURL)};
NSNotification *notification = [NSNotification notificationWithName:FSAudioStreamStateChangeNotification object:self userInfo:userInfo];
[[NSNotificationCenter defaultCenter] postNotification:notification];
[NSTimer scheduledTimerWithTimeInterval:0
target:self
selector:@selector(notifyRetrievingURL)
userInfo:nil
repeats:NO];
return;
}
if ([self.playlistItems count] > 0) {
if (self.currentPlaylistItem.originatingUrl) {
self.audioStream.url = self.currentPlaylistItem.originatingUrl;
} else {
self.audioStream.url = self.currentPlaylistItem.url;
}
} else {
self.audioStream.url = self.url;
}
if (self.onStateChange) {
self.audioStream.onStateChange = self.onStateChange;
}
if (self.onMetaDataAvailable) {
self.audioStream.onMetaDataAvailable = self.onMetaDataAvailable;
}
if (self.onFailure) {
self.audioStream.onFailure = self.onFailure;
}
FSAudioStream *stream = self.audioStream;
if (self.enableDebugOutput) {
NSLog(@"Playing %@", stream);
}
[stream play];
}
- (void)playFromURL:(NSURL*)url
{
if (!url) {
return;
}
[_playlistItems removeAllObjects];
[self stop];
self.url = url;
[self play];
}
- (void)playFromPlaylist:(NSArray *)playlist
{
[self playFromPlaylist:playlist itemIndex:0];
}
- (void)playFromPlaylist:(NSArray *)playlist itemIndex:(NSUInteger)index
{
[self stop];
self.playlistItems = [[NSMutableArray alloc] init];
_streams = [[NSMutableArray alloc] init];
self.currentPlaylistItemIndex = 0;
[self.playlistItems addObjectsFromArray:playlist];
for (FSPlaylistItem *item in playlist) {
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
proxy.url = item.url;
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] playFromPlaylist. Adding stream proxy for %@", __LINE__, proxy.url);
}
[_streams addObject:proxy];
}
[self playItemAtIndex:index];
}
- (void)playItemAtIndex:(NSUInteger)index
{
NSUInteger count = [self countOfItems];
if (count == 0) {
return;
}
if (index >= count) {
return;
}
[self.audioStream stop];
self.currentPlaylistItemIndex = index;
self.readyToPlay = YES;
[self deactivateInactivateStreams:index];
[self play];
}
- (NSUInteger)countOfItems
{
return [self.playlistItems count];
}
- (void)addItem:(FSPlaylistItem *)item
{
if (!item) {
return;
}
[self.playlistItems addObject:item];
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
proxy.url = item.url;
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] addItem. Adding stream proxy for %@", __LINE__, proxy.url);
}
[_streams addObject:proxy];
}
- (void)insertItem:(FSPlaylistItem *)item atIndex:(NSInteger)index
{
if (!item) {
return;
}
if (index > self.playlistItems.count) {
return;
}
if(self.playlistItems.count == 0 && index == 0) {
[self addItem:item];
return;
}
[self.playlistItems insertObject:item
atIndex:index];
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
proxy.url = item.url;
[_streams insertObject:proxy
atIndex:index];
if(index <= self.currentPlaylistItemIndex) {
_currentPlaylistItemIndex++;
}
}
- (void)replaceItemAtIndex:(NSUInteger)index withItem:(FSPlaylistItem *)item
{
NSUInteger count = [self countOfItems];
if (count == 0) {
return;
}
if (index >= count) {
return;
}
if (self.currentPlaylistItemIndex == index) {
// If the item is currently playing, do not allow the replacement
return;
}
[self.playlistItems replaceObjectAtIndex:index withObject:item];
FSAudioStreamProxy *proxy = [[FSAudioStreamProxy alloc] initWithAudioController:self];
proxy.url = item.url;
[_streams replaceObjectAtIndex:index withObject:proxy];
}
- (void)moveItemAtIndex:(NSUInteger)from toIndex:(NSUInteger)to {
NSUInteger count = [self countOfItems];
if (count == 0) {
return;
}
if (from >= count || to >= count) {
return;
}
if(from == self.currentPlaylistItemIndex) {
_currentPlaylistItemIndex = to;
}
else if(from < self.currentPlaylistItemIndex && to > self.currentPlaylistItemIndex) {
_currentPlaylistItemIndex--;
}
else if(from > self.currentPlaylistItemIndex && to <= self.currentPlaylistItemIndex) {
_currentPlaylistItemIndex++;
}
id object = [self.playlistItems objectAtIndex:from];
[self.playlistItems removeObjectAtIndex:from];
[self.playlistItems insertObject:object atIndex:to];
id obj = [_streams objectAtIndex:from];
[_streams removeObjectAtIndex:from];
[_streams insertObject:obj atIndex:to];
}
- (void)removeItemAtIndex:(NSUInteger)index
{
NSUInteger count = [self countOfItems];
if (count == 0) {
return;
}
if (index >= count) {
return;
}
if (self.currentPlaylistItemIndex == index && self.isPlaying) {
// If the item is currently playing, do not allow the removal
return;
}
FSPlaylistItem *current = self.currentPlaylistItem;
[self.playlistItems removeObjectAtIndex:index];
if (self.enableDebugOutput) {
FSAudioStreamProxy *proxy = [_streams objectAtIndex:index];
NSLog(@"[FSAudioController.m:%i] removeItemAtIndex. Removing stream proxy %@", __LINE__, proxy.url);
}
[_streams removeObjectAtIndex:index];
// Update the current playlist item to be correct after the removal
NSUInteger itemIndex = 0;
for (FSPlaylistItem *item in self.playlistItems) {
if (item == current) {
self.currentPlaylistItemIndex = itemIndex;
break;
}
itemIndex++;
}
}
- (void)stop
{
if ([_streams count] > 0) {
// Avoid creating an instance if we don't have it
[self.audioStream stop];
}
[_checkContentTypeRequest cancel];
[_parsePlaylistRequest cancel];
[_parseRssPodcastFeedRequest cancel];
self.readyToPlay = NO;
}
- (void)pause
{
[self.audioStream pause];
}
-(BOOL)hasMultiplePlaylistItems
{
return ([self.playlistItems count] > 1);
}
-(BOOL)hasNextItem
{
return [self hasMultiplePlaylistItems] && (self.currentPlaylistItemIndex + 1 < [self.playlistItems count]);
}
-(BOOL)hasPreviousItem
{
return ([self hasMultiplePlaylistItems] && (self.currentPlaylistItemIndex != 0));
}
-(void)playNextItem
{
if ([self hasNextItem]) {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] playNexItem. Stopping stream %@", __LINE__, self.audioStream.url);
}
[self.audioStream stop];
[self deactivateInactivateStreams:self.currentPlaylistItemIndex];
self.currentPlaylistItemIndex = self.currentPlaylistItemIndex + 1;
[self play];
}
}
-(void)playPreviousItem
{
if ([self hasPreviousItem]) {
if (self.enableDebugOutput) {
NSLog(@"[FSAudioController.m:%i] playPreviousItem. Stopping stream %@", __LINE__, self.audioStream.url);
}
[self.audioStream stop];
[self deactivateInactivateStreams:self.currentPlaylistItemIndex];
self.currentPlaylistItemIndex = self.currentPlaylistItemIndex - 1;
[self play];
}
}
/*
* =======================================
* Properties
* =======================================
*/
- (void)setVolume:(float)volume
{
self.outputVolume = volume;
self.needToSetVolume = YES;
if ([_streams count] > 0) {
self.audioStream.volume = self.outputVolume;
}
}
- (float)volume
{
return self.outputVolume;
}
- (void)setUrl:(NSURL *)url
{
[self stop];
if (url) {
NSURL *copyOfURL = [url copy];
_url = copyOfURL;
self.checkContentTypeRequest.url = _url;
self.parsePlaylistRequest.url = _url;
self.parseRssPodcastFeedRequest.url = _url;
if ([_url isFileURL]) {
/*
* Local file URLs can be directly played
*/
self.readyToPlay = YES;
}
} else {
_url = nil;
}
}
- (NSURL* )url
{
if (!_url) {
return nil;
}
NSURL *copyOfURL = [_url copy];
return copyOfURL;
}
- (FSAudioStream *)activeStream
{
if ([_streams count] > 0) {
return self.audioStream;
}
return nil;
}
- (FSPlaylistItem *)currentPlaylistItem
{
if (self.readyToPlay) {
if ([self.playlistItems count] > 0) {
FSPlaylistItem *playlistItem = (self.playlistItems)[self.currentPlaylistItemIndex];
return playlistItem;
}
}
return nil;
}
- (void (^)(FSAudioStreamState state))onStateChange
{
return _onStateChangeBlock;
}
- (void (^)(NSDictionary *metaData))onMetaDataAvailable
{
return _onMetaDataAvailableBlock;
}
- (void (^)(FSAudioStreamError error, NSString *errorDescription))onFailure
{
return _onFailureBlock;
}
- (void)setOnStateChange:(void (^)(FSAudioStreamState))newOnStateValue
{
_onStateChangeBlock = newOnStateValue;
if ([_streams count] > 0) {
self.audioStream.onStateChange = _onStateChangeBlock;
}
}
- (void)setOnMetaDataAvailable:(void (^)(NSDictionary *))newOnMetaDataAvailableValue
{
_onMetaDataAvailableBlock = newOnMetaDataAvailableValue;
if ([_streams count] > 0) {
self.audioStream.onMetaDataAvailable = _onMetaDataAvailableBlock;
}
}
- (void)setOnFailure:(void (^)(FSAudioStreamError error, NSString *errorDescription))newOnFailureValue
{
_onFailureBlock = newOnFailureValue;
if ([_streams count] > 0) {
self.audioStream.onFailure = _onFailureBlock;
}
}
@end

View File

@ -0,0 +1,600 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudioTypes.h>
/**
* The major version of the current release.
*/
#define FREESTREAMER_VERSION_MAJOR 4
/**
* The minor version of the current release.
*/
#define FREESTREAMER_VERSION_MINOR 0
/**
* The reversion of the current release
*/
#define FREESTREAMER_VERSION_REVISION 0
/**
* Follow this notification for the audio stream state changes.
*/
extern NSString* const FSAudioStreamStateChangeNotification;
extern NSString* const FSAudioStreamNotificationKey_State;
/**
* Follow this notification for the audio stream errors.
*/
extern NSString* const FSAudioStreamErrorNotification;
extern NSString* const FSAudioStreamNotificationKey_Error;
/**
* Follow this notification for the audio stream metadata.
*/
extern NSString* const FSAudioStreamMetaDataNotification;
extern NSString* const FSAudioStreamNotificationKey_MetaData;
/**
* The audio stream state.
*/
typedef NS_ENUM(NSInteger, FSAudioStreamState) {
/**
* Retrieving URL.
*/
kFsAudioStreamRetrievingURL,
/**
* Stopped.
*/
kFsAudioStreamStopped,
/**
* Buffering.
*/
kFsAudioStreamBuffering,
/**
* Playing.
*/
kFsAudioStreamPlaying,
/**
* Paused.
*/
kFsAudioStreamPaused,
/**
* Seeking.
*/
kFsAudioStreamSeeking,
/**
* The stream has received all the data for a file.
*/
kFSAudioStreamEndOfFile,
/**
* Failed.
*/
kFsAudioStreamFailed,
/**
* Started retrying.
*/
kFsAudioStreamRetryingStarted,
/**
* Retrying succeeded.
*/
kFsAudioStreamRetryingSucceeded,
/**
* Retrying failed.
*/
kFsAudioStreamRetryingFailed,
/**
* Playback completed.
*/
kFsAudioStreamPlaybackCompleted,
/**
* Unknown state.
*/
kFsAudioStreamUnknownState
};
/**
* The audio stream errors.
*/
typedef NS_ENUM(NSInteger, FSAudioStreamError) {
/**
* No error.
*/
kFsAudioStreamErrorNone = 0,
/**
* Error opening the stream.
*/
kFsAudioStreamErrorOpen = 1,
/**
* Error parsing the stream.
*/
kFsAudioStreamErrorStreamParse = 2,
/**
* Network error.
*/
kFsAudioStreamErrorNetwork = 3,
/**
* Unsupported format.
*/
kFsAudioStreamErrorUnsupportedFormat = 4,
/**
* Stream buffered too often.
*/
kFsAudioStreamErrorStreamBouncing = 5,
/**
* Stream playback was terminated by the operating system.
*/
kFsAudioStreamErrorTerminated = 6
};
@protocol FSPCMAudioStreamDelegate;
@class FSAudioStreamPrivate;
/**
* The audio stream playback position.
*/
typedef struct {
unsigned minute;
unsigned second;
/**
* Playback time in seconds.
*/
float playbackTimeInSeconds;
/**
* Position within the stream, where 0 is the beginning
* and 1.0 is the end.
*/
float position;
} FSStreamPosition;
/**
* The audio stream seek byte offset.
*/
typedef struct {
UInt64 start;
UInt64 end;
/**
* Position within the stream, where 0 is the beginning
* and 1.0 is the end.
*/
float position;
} FSSeekByteOffset;
/**
* Audio levels.
*/
typedef struct {
Float32 averagePower;
Float32 peakPower;
} FSLevelMeterState;
/**
* The low-level stream configuration.
*/
@interface FSStreamConfiguration : NSObject {
}
/**
* The number of buffers.
*/
@property (nonatomic,assign) unsigned bufferCount;
/**
* The size of each buffer.
*/
@property (nonatomic,assign) unsigned bufferSize;
/**
* The number of packet descriptions.
*/
@property (nonatomic,assign) unsigned maxPacketDescs;
/**
* The HTTP connection buffer size.
*/
@property (nonatomic,assign) unsigned httpConnectionBufferSize;
/**
* The output sample rate.
*/
@property (nonatomic,assign) double outputSampleRate;
/**
* The number of output channels.
*/
@property (nonatomic,assign) long outputNumChannels;
/**
* The interval within the stream may enter to the buffering state before it fails.
*/
@property (nonatomic,assign) int bounceInterval;
/**
* The number of times the stream may enter the buffering state before it fails.
*/
@property (nonatomic,assign) int maxBounceCount;
/**
* The stream must start within this seconds before it fails.
*/
@property (nonatomic,assign) int startupWatchdogPeriod;
/**
* Allow buffering of this many bytes before the cache is full.
*/
@property (nonatomic,assign) int maxPrebufferedByteCount;
/**
* Calculate prebuffer sizes dynamically using the stream bitrate in seconds instead of bytes.
*/
@property (nonatomic,assign) BOOL usePrebufferSizeCalculationInSeconds;
/**
* Calculate prebuffer sizes using the packet counts.
*/
@property (nonatomic,assign) BOOL usePrebufferSizeCalculationInPackets;
/**
* Require buffering of this many bytes before the playback can start for a continuous stream.
*/
@property (nonatomic,assign) float requiredPrebufferSizeInSeconds;
/**
* Require buffering of this many bytes before the playback can start for a continuous stream.
*/
@property (nonatomic,assign) int requiredInitialPrebufferedByteCountForContinuousStream;
/**
* Require buffering of this many bytes before the playback can start a non-continuous stream.
*/
@property (nonatomic,assign) int requiredInitialPrebufferedByteCountForNonContinuousStream;
/**
* Require buffering of this many packets before the playback can start.
*/
@property (nonatomic,assign) int requiredInitialPrebufferedPacketCount;
/**
* The HTTP user agent used for stream operations.
*/
@property (nonatomic,strong) NSString *userAgent;
/**
* The directory used for caching the streamed files.
*/
@property (nonatomic,strong) NSString *cacheDirectory;
/**
* The HTTP headers that are appended to the request when the streaming starts. Notice
* that the headers override any headers previously set by FreeStreamer.
*/
@property (nonatomic,strong) NSDictionary *predefinedHttpHeaderValues;
/**
* The property determining if caching the streams to the disk is enabled.
*/
@property (nonatomic,assign) BOOL cacheEnabled;
/**
* The property determining if seeking from the audio packets stored in cache is enabled.
* The benefit is that seeking is faster in the case the audio packets are already cached in memory.
*/
@property (nonatomic,assign) BOOL seekingFromCacheEnabled;
/**
* The property determining if FreeStreamer should handle audio session automatically.
* Leave it on if you don't want to handle the audio session by yourself.
*/
@property (nonatomic,assign) BOOL automaticAudioSessionHandlingEnabled;
/**
* The property enables time and pitch conversion for the audio queue. Put it on
* if you want to use the play rate setting.
*/
@property (nonatomic,assign) BOOL enableTimeAndPitchConversion;
/**
* Requires the content type given by the server to match an audio content type.
*/
@property (nonatomic,assign) BOOL requireStrictContentTypeChecking;
/**
* The maximum size of the disk cache in bytes.
*/
@property (nonatomic,assign) int maxDiskCacheSize;
@end
/**
* Statistics on the stream state.
*/
@interface FSStreamStatistics : NSObject {
}
/**
* Time when the statistics were gathered.
*/
@property (nonatomic,strong) NSDate *snapshotTime;
/**
* Time in a pretty format.
*/
@property (nonatomic,readonly) NSString *snapshotTimeFormatted;
/**
* Audio stream packet count.
*/
@property (nonatomic,assign) NSUInteger audioStreamPacketCount;
/**
* Audio queue used buffers count.
*/
@property (nonatomic,assign) NSUInteger audioQueueUsedBufferCount;
/**
* Audio stream PCM packet queue count.
*/
@property (nonatomic,assign) NSUInteger audioQueuePCMPacketQueueCount;
@end
NSString* freeStreamerReleaseVersion(void);
/**
* FSAudioStream is a class for streaming audio files from an URL.
* It must be directly fed with an URL, which contains audio. That is,
* playlists or other non-audio formats yield an error.
*
* To start playback, the stream must be either initialized with an URL
* or the playback URL can be set with the url property. The playback
* is started with the play method. It is possible to pause or stop
* the stream with the respective methods.
*
* Non-continuous streams (audio streams with a known duration) can be
* seeked with the seekToPosition method.
*
* Note that FSAudioStream is not designed to be thread-safe! That means
* that using the streamer from multiple threads without syncronization
* could cause problems. It is recommended to keep the streamer in the
* main thread and call the streamer methods only from the main thread
* (consider using performSelectorOnMainThread: if calls from multiple
* threads are needed).
*/
@interface FSAudioStream : NSObject {
FSAudioStreamPrivate *_private;
}
/**
* Initializes the audio stream with an URL.
*
* @param url The URL from which the stream data is retrieved.
*/
- (id)initWithUrl:(NSURL *)url;
/**
* Initializes the stream with a configuration.
*
* @param configuration The stream configuration.
*/
- (id)initWithConfiguration:(FSStreamConfiguration *)configuration;
/**
* Starts preload the stream. If no preload URL is
* defined, an error will occur.
*/
- (void)preload;
/**
* Starts playing the stream. If no playback URL is
* defined, an error will occur.
*/
- (void)play;
/**
* Starts playing the stream from the given URL.
*
* @param url The URL from which the stream data is retrieved.
*/
- (void)playFromURL:(NSURL*)url;
/**
* Starts playing the stream from the given offset.
* The offset can be retrieved from the stream with the
* currentSeekByteOffset property.
*
* @param offset The offset where to start playback from.
*/
- (void)playFromOffset:(FSSeekByteOffset)offset;
/**
* Stops the stream playback.
*/
- (void)stop;
/**
* If the stream is playing, the stream playback is paused upon calling pause.
* Otherwise (the stream is paused), calling pause will continue the playback.
*/
- (void)pause;
/**
* Rewinds the stream. Only possible for continuous streams.
*
* @param seconds Seconds to rewind the stream.
*/
- (void)rewind:(unsigned)seconds;
/**
* Seeks the stream to a given position. Requires a non-continuous stream
* (a stream with a known duration).
*
* @param position The stream position to seek to.
*/
- (void)seekToPosition:(FSStreamPosition)position;
/**
* Sets the audio stream playback rate from 0.5 to 2.0.
* Value 1.0 means the normal playback rate. Values below
* 1.0 means a slower playback rate than usual and above
* 1.0 a faster playback rate. Notice that using a faster
* playback rate than 1.0 may mean that you have to increase
* the buffer sizes for the stream still to play.
*
* The play rate has only effect if the stream is playing.
*
* @param playRate The playback rate.
*/
- (void)setPlayRate:(float)playRate;
/**
* Returns the playback status: YES if the stream is playing, NO otherwise.
*/
- (BOOL)isPlaying;
/**
* Cleans all cached data from the persistent storage.
*/
- (void)expungeCache;
/**
* The stream URL.
*/
@property (nonatomic,assign) NSURL *url;
/**
* Determines if strict content type checking is required. If the audio stream
* cannot determine that the stream is actually an audio stream, the stream
* does not play. Disabling strict content type checking bypasses the
* stream content type checks and tries to play the stream regardless
* of the content type information given by the server.
*/
@property (nonatomic,assign) BOOL strictContentTypeChecking;
/**
* Set an output file to store the stream contents to a file.
*/
@property (nonatomic,assign) NSURL *outputFile;
/**
* Sets a default content type for the stream. Used if
* the stream content type is not available.
*/
@property (nonatomic,assign) NSString *defaultContentType;
/**
* The property has the content type of the stream, for instance audio/mpeg.
*/
@property (nonatomic,readonly) NSString *contentType;
/**
* The property has the suggested file extension for the stream based on the stream content type.
*/
@property (nonatomic,readonly) NSString *suggestedFileExtension;
/**
* Sets a default content length for the stream. Used if
* the stream content-length is not available.
*/
@property (nonatomic, assign) UInt64 defaultContentLength;
/**
* The property has the content length of the stream (in bytes). The length is zero if
* the stream is continuous.
*/
@property (nonatomic,readonly) UInt64 contentLength;
/**
* The number of bytes of audio data. Notice that this may differ
* from the number of bytes the server returns for the content length!
* For instance audio file meta data is excluded from the count.
* Effectively you can use this property for seeking calculations.
*/
@property (nonatomic,readonly) UInt64 audioDataByteCount;
/**
* This property has the current playback position, if the stream is non-continuous.
* The current playback position cannot be determined for continuous streams.
*/
@property (nonatomic,readonly) FSStreamPosition currentTimePlayed;
/**
* This property has the duration of the stream, if the stream is non-continuous.
* Continuous streams do not have a duration.
*/
@property (nonatomic,readonly) FSStreamPosition duration;
/**
* This property has the current seek byte offset of the stream, if the stream is non-continuous.
* Continuous streams do not have a seek byte offset.
*/
@property (nonatomic,readonly) FSSeekByteOffset currentSeekByteOffset;
/**
* This property has the bit rate of the stream. The bit rate is initially 0,
* before the stream has processed enough packets to calculate the bit rate.
*/
@property (nonatomic,readonly) float bitRate;
/**
* The property is true if the stream is continuous (no known duration).
*/
@property (nonatomic,readonly) BOOL continuous;
/**
* The property is true if the stream has been cached locally.
*/
@property (nonatomic,readonly) BOOL cached;
/**
* This property has the number of bytes buffered for this stream.
*/
@property (nonatomic,readonly) size_t prebufferedByteCount;
/**
* This property holds the current playback volume of the stream,
* from 0.0 to 1.0.
*
* Note that the overall volume is still constrained by the volume
* set by the user! So the actual volume cannot be higher
* than the volume currently set by the user. For example, if
* requesting a volume of 0.5, then the volume will be 50%
* lower than the current playback volume set by the user.
*/
@property (nonatomic,assign) float volume;
/**
* The current size of the disk cache.
*/
@property (nonatomic,readonly) unsigned long long totalCachedObjectsSize;
/**
* The property determines the amount of times the stream has tried to retry the playback
* in case of failure.
*/
@property (nonatomic,readonly) NSUInteger retryCount;
/**
* Holds the maximum amount of playback retries that will be
* performed before entering kFsAudioStreamRetryingFailed state.
* Default is 3.
*/
@property (nonatomic,assign) NSUInteger maxRetryCount;
/**
* The property determines the current audio levels.
*/
@property (nonatomic,readonly) FSLevelMeterState levels;
/**
* This property holds the current statistics for the stream state.
*/
@property (nonatomic,readonly) FSStreamStatistics *statistics;
/**
* Called upon completion of the stream. Note that for continuous
* streams this is never called.
*/
@property (copy) void (^onCompletion)(void);
/**
* Called upon a state change.
*/
@property (copy) void (^onStateChange)(FSAudioStreamState state);
/**
* Called upon a meta data is available.
*/
@property (copy) void (^onMetaDataAvailable)(NSDictionary *metadata);
/**
* Called upon a failure.
*/
@property (copy) void (^onFailure)(FSAudioStreamError error, NSString *errorDescription);
/**
* The property has the low-level stream configuration.
*/
@property (readonly) FSStreamConfiguration *configuration;
/**
* Delegate.
*/
@property (nonatomic,unsafe_unretained) IBOutlet id<FSPCMAudioStreamDelegate> delegate;
@end
/**
* To access the PCM audio data, use this delegate.
*/
@protocol FSPCMAudioStreamDelegate <NSObject>
@optional
/**
* Called when there are PCM audio samples available. Do not do any blocking operations
* when you receive the data. Instead, copy the data and process it so that the
* main event loop doesn't block. Failing to do so may cause glitches to the audio playback.
*
* Notice that the delegate callback may occur from other than the main thread so make
* sure your delegate code is thread safe.
*
* @param audioStream The audio stream the samples are from.
* @param samples The samples as a buffer list.
* @param frames The number of frames.
* @param description Description of the data provided.
*/
- (void)audioStream:(FSAudioStream *)audioStream samplesAvailable:(AudioBufferList *)samples frames:(UInt32)frames description: (AudioStreamPacketDescription)description;
@end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <Foundation/Foundation.h>
/**
* Content type format.
*/
typedef NS_ENUM(NSInteger, FSFileFormat) {
/**
* Unknown format.
*/
kFSFileFormatUnknown = 0,
/**
* M3U playlist.
*/
kFSFileFormatM3UPlaylist,
/**
* PLS playlist.
*/
kFSFileFormatPLSPlaylist,
/**
* XML file.
*/
kFSFileFormatXML,
/**
* MP3 file.
*/
kFSFileFormatMP3,
/**
* WAVE file.
*/
kFSFileFormatWAVE,
/**
* AIFC file.
*/
kFSFileFormatAIFC,
/**
* AIFF file.
*/
kFSFileFormatAIFF,
/**
* M4A file.
*/
kFSFileFormatM4A,
/**
* MPEG4 file.
*/
kFSFileFormatMPEG4,
/**
* CAF file.
*/
kFSFileFormatCAF,
/**
* AAC_ADTS file.
*/
kFSFileFormatAAC_ADTS,
/**
* Total number of formats.
*/
kFSFileFormatCount
};
/**
* FSCheckContentTypeRequest is a class for checking the content type
* of a URL. It makes an HTTP HEAD request and parses the header information
* from the server. The resulting format is stored in the format property.
*
* To use the class, define the URL for checking the content type using
* the url property. Then, define the onCompletion and onFailure handlers.
* To start the request, use the start method.
*/
@interface FSCheckContentTypeRequest : NSObject <NSURLSessionDelegate> {
NSURLSessionTask *_task;
FSFileFormat _format;
NSString *_contentType;
BOOL _playlist;
BOOL _xml;
}
/**
* The URL of this request.
*/
@property (nonatomic,copy) NSURL *url;
/**
* Called when the content type determination is completed.
*/
@property (copy) void (^onCompletion)(void);
/**
* Called if the content type determination failed.
*/
@property (copy) void (^onFailure)(void);
/**
* Contains the format of the URL upon completion of the request.
*/
@property (nonatomic,readonly) FSFileFormat format;
/**
* Containts the content type of the URL upon completion of the request.
*/
@property (nonatomic,readonly) NSString *contentType;
/**
* The property is true if the URL contains a playlist.
*/
@property (nonatomic,readonly) BOOL playlist;
/**
* The property is true if the URL contains XML data.
*/
@property (nonatomic,readonly) BOOL xml;
/**
* Starts the request.
*/
- (void)start;
/**
* Cancels the request.
*/
- (void)cancel;
@end

View File

@ -0,0 +1,236 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import "FSCheckContentTypeRequest.h"
@interface FSCheckContentTypeRequest ()
- (BOOL)guessContentTypeByUrl:(NSURLResponse *)response;
@end
@implementation FSCheckContentTypeRequest
- (id)init
{
self = [super init];
if (self) {
_format = kFSFileFormatUnknown;
_playlist = NO;
_xml = NO;
}
return self;
}
- (void)start
{
if (_task) {
return;
}
_format = kFSFileFormatUnknown;
_playlist = NO;
_contentType = @"";
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:10.0];
[request setHTTPMethod:@"HEAD"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
@synchronized (self) {
_task = [session dataTaskWithRequest:request];
}
[_task resume];
if (!_task) {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSCheckContentTypeRequest: Unable to open connection for URL: %@", _url);
#endif
self.onFailure();
return;
}
}
- (void)cancel
{
if (!_task) {
return;
}
@synchronized (self) {
[_task cancel];
_task = nil;
}
}
/*
* =======================================
* Properties
* =======================================
*/
- (FSFileFormat)format
{
return _format;
}
- (NSString *)contentType
{
return _contentType;
}
- (BOOL)playlist
{
return _playlist;
}
- (BOOL)xml
{
return _xml;
}
/*
* =======================================
* NSURLSessionDelegate
* =======================================
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
_contentType = response.MIMEType;
_format = kFSFileFormatUnknown;
_playlist = NO;
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (statusCode >= 200 && statusCode <= 299) {
// Only use the content type if the response indicated success (2xx)
if ([_contentType isEqualToString:@"audio/mpeg"]) {
_format = kFSFileFormatMP3;
} else if ([_contentType isEqualToString:@"audio/x-wav"]) {
_format = kFSFileFormatWAVE;
} else if ([_contentType isEqualToString:@"audio/x-aifc"]) {
_format = kFSFileFormatAIFC;
} else if ([_contentType isEqualToString:@"audio/x-aiff"]) {
_format = kFSFileFormatAIFF;
} else if ([_contentType isEqualToString:@"audio/x-m4a"]) {
_format = kFSFileFormatM4A;
} else if ([_contentType isEqualToString:@"audio/mp4"]) {
_format = kFSFileFormatMPEG4;
} else if ([_contentType isEqualToString:@"audio/x-caf"]) {
_format = kFSFileFormatCAF;
} else if ([_contentType isEqualToString:@"audio/aac"] ||
[_contentType isEqualToString:@"audio/aacp"]) {
_format = kFSFileFormatAAC_ADTS;
} else if ([_contentType isEqualToString:@"audio/x-mpegurl"] ||
[_contentType isEqualToString:@"application/x-mpegurl"]) {
_format = kFSFileFormatM3UPlaylist;
_playlist = YES;
} else if ([_contentType isEqualToString:@"audio/x-scpls"] ||
[_contentType isEqualToString:@"application/pls+xml"]) {
_format = kFSFileFormatPLSPlaylist;
_playlist = YES;
} else if ([_contentType isEqualToString:@"text/xml"] ||
[_contentType isEqualToString:@"application/xml"]) {
_format = kFSFileFormatXML;
_xml = YES;
} else {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSCheckContentTypeRequest: Cannot resolve %@, guessing the content type by URL: %@", _contentType, _url);
#endif
[self guessContentTypeByUrl:response];
}
} else {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSCheckContentTypeRequest: Invalid HTTP status code received %li, guessing the content type by URL: %@", (long)statusCode, _url);
#endif
[self guessContentTypeByUrl:response];
}
_task = nil;
self.onCompletion();
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
@synchronized (self) {
_task = nil;
_format = kFSFileFormatUnknown;
_playlist = NO;
}
// Still, try if we could resolve the content type by the URL
if ([self guessContentTypeByUrl:nil]) {
self.onCompletion();
} else {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSCheckContentTypeRequest: Unable to determine content-type for the URL: %@, error %@", _url, [error localizedDescription]);
#endif
self.onFailure();
}
}
/*
* =======================================
* Private
* =======================================
*/
- (BOOL)guessContentTypeByUrl:(NSURLResponse *)response
{
/* The server did not provide meaningful content type;
last resort: check the file suffix, if there is one */
NSString *absoluteUrl;
if (response) {
absoluteUrl = [response.URL absoluteString];
} else {
absoluteUrl = [_url absoluteString];
}
if ([absoluteUrl hasSuffix:@".mp3"]) {
_format = kFSFileFormatMP3;
} else if ([absoluteUrl hasSuffix:@".mp4"]) {
_format = kFSFileFormatMPEG4;
} else if ([absoluteUrl hasSuffix:@".m3u"]) {
_format = kFSFileFormatM3UPlaylist;
_playlist = YES;
} else if ([absoluteUrl hasSuffix:@".pls"]) {
_format = kFSFileFormatPLSPlaylist;
_playlist = YES;
} else if ([absoluteUrl hasSuffix:@".xml"]) {
_format = kFSFileFormatXML;
_xml = YES;
} else {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSCheckContentTypeRequest: Failed to determine content type from the URL: %@", _url);
#endif
/*
* Failed to guess the content type based on the URL.
*/
return NO;
}
/*
* We have determined a content-type.
*/
return YES;
}
@end

View File

@ -0,0 +1,71 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <Foundation/Foundation.h>
/**
* The playlist format.
*/
typedef NS_ENUM(NSInteger, FSPlaylistFormat) {
/**
* Unknown playlist format.
*/
kFSPlaylistFormatNone,
/**
* M3U playlist.
*/
kFSPlaylistFormatM3U,
/**
* PLS playlist.
*/
kFSPlaylistFormatPLS
};
/**
* FSParsePlaylistRequest is a class for parsing a playlist. It supports
* the M3U and PLS formats.
*
* To use the class, define the URL for retrieving the playlist using
* the url property. Then, define the onCompletion and onFailure handlers.
* To start the request, use the start method.
*/
@interface FSParsePlaylistRequest : NSObject<NSURLSessionDelegate> {
NSURLSessionTask *_task;
NSInteger _httpStatus;
NSMutableData *_receivedData;
NSMutableArray *_playlistItems;
FSPlaylistFormat _format;
}
/**
* The URL of this request.
*/
@property (nonatomic,copy) NSURL *url;
/**
* Called when the playlist parsing is completed.
*/
@property (copy) void (^onCompletion)(void);
/**
* Called if the playlist parsing failed.
*/
@property (copy) void (^onFailure)(void);
/**
* The playlist items stored in the FSPlaylistItem class.
*/
@property (readonly) NSMutableArray *playlistItems;
/**
* Starts the request.
*/
- (void)start;
/**
* Cancels the request.
*/
- (void)cancel;
@end

View File

@ -0,0 +1,325 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import "FSParsePlaylistRequest.h"
#import "FSPlaylistItem.h"
@interface FSParsePlaylistRequest ()
- (void)parsePlaylistFromData:(NSData *)data;
- (void)parsePlaylistM3U:(NSString *)playlist;
- (void)parsePlaylistPLS:(NSString *)playlist;
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl;
@property (readonly) FSPlaylistFormat format;
@end
@implementation FSParsePlaylistRequest
- (id)init
{
self = [super init];
if (self) {
}
return self;
}
- (void)start
{
if (_task) {
return;
}
NSURLRequest *request = [NSURLRequest requestWithURL:self.url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
@synchronized (self) {
_receivedData = [NSMutableData data];
_task = [session dataTaskWithRequest:request];
_playlistItems = [[NSMutableArray alloc] init];
_format = kFSPlaylistFormatNone;
}
[_task resume];
}
- (void)cancel
{
if (!_task) {
return;
}
@synchronized (self) {
[_task cancel];
_task = nil;
}
}
/*
* =======================================
* Properties
* =======================================
*/
- (NSMutableArray *)playlistItems
{
return [_playlistItems copy];
}
- (FSPlaylistFormat)format
{
return _format;
}
/*
* =======================================
* Private
* =======================================
*/
- (void)parsePlaylistFromData:(NSData *)data
{
NSString *playlistData = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
if (_format == kFSPlaylistFormatM3U) {
[self parsePlaylistM3U:playlistData];
if ([_playlistItems count] == 0) {
// If we failed to grab any playlist items, still try
// to parse it in another format; perhaps the server
// mistakingly identified the playlist format
[self parsePlaylistPLS:playlistData];
}
} else if (_format == kFSPlaylistFormatPLS) {
[self parsePlaylistPLS:playlistData];
if ([_playlistItems count] == 0) {
// If we failed to grab any playlist items, still try
// to parse it in another format; perhaps the server
// mistakingly identified the playlist format
[self parsePlaylistM3U:playlistData];
}
}
if ([_playlistItems count] == 0) {
/*
* Fail if we failed to parse any items from the playlist.
*/
self.onFailure();
}
}
- (void)parsePlaylistM3U:(NSString *)playlist
{
[_playlistItems removeAllObjects];
for (NSString *line in [playlist componentsSeparatedByString:@"\n"]) {
if ([line hasPrefix:@"#"]) {
/* metadata, skip */
continue;
}
if ([line hasPrefix:@"http://"] ||
[line hasPrefix:@"https://"]) {
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
item.url = [NSURL URLWithString:[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
[_playlistItems addObject:item];
} else if ([line hasPrefix:@"file://"]) {
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
item.url = [self parseLocalFileUrl:line];
[_playlistItems addObject:item];
}
}
}
- (void)parsePlaylistPLS:(NSString *)playlist
{
[_playlistItems removeAllObjects];
NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
size_t i = 0;
for (NSString *rawLine in [playlist componentsSeparatedByString:@"\n"]) {
NSString *line = [rawLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (i == 0) {
if ([[line lowercaseString] hasPrefix:@"[playlist]"]) {
i++;
continue;
} else {
// Invalid playlist; the first line should indicate that this is a playlist
return;
}
}
// Ignore empty lines
if ([line length] == 0) {
i++;
continue;
}
// Not an empty line; so expect that this is a key/value pair
NSRange r = [line rangeOfString:@"="];
// Invalid format, key/value pair not found
if (r.length == 0) {
return;
}
NSString *key = [[line substringToIndex:r.location] lowercaseString];
NSString *value = [line substringFromIndex:r.location + 1];
props[key] = value;
i++;
}
NSInteger numItems = [[props valueForKey:@"numberofentries"] integerValue];
if (numItems == 0) {
// Invalid playlist; number of playlist items not defined
return;
}
for (i=0; i < numItems; i++) {
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
NSString *title = [props valueForKey:[NSString stringWithFormat:@"title%lu", (i+1)]];
item.title = title;
NSString *file = [props valueForKey:[NSString stringWithFormat:@"file%lu", (i+1)]];
if ([file hasPrefix:@"http://"] ||
[file hasPrefix:@"https://"]) {
item.url = [NSURL URLWithString:file];
[_playlistItems addObject:item];
} else if ([file hasPrefix:@"file://"]) {
item.url = [self parseLocalFileUrl:file];
[_playlistItems addObject:item];
}
}
}
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl
{
// Resolve the local bundle URL
NSString *path = [fileUrl substringFromIndex:7];
NSRange range = [path rangeOfString:@"." options:NSBackwardsSearch];
NSString *fileName = [path substringWithRange:NSMakeRange(0, range.location)];
NSString *suffix = [path substringWithRange:NSMakeRange(range.location + 1, [path length] - [fileName length] - 1)];
return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:suffix]];
}
/*
* =======================================
* NSURLSessionDelegate
* =======================================
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
_httpStatus = [httpResponse statusCode];
NSString *contentType = response.MIMEType;
NSString *absoluteUrl = [response.URL absoluteString];
_format = kFSPlaylistFormatNone;
if ([contentType isEqualToString:@"audio/x-mpegurl"] ||
[contentType isEqualToString:@"application/x-mpegurl"]) {
_format = kFSPlaylistFormatM3U;
} else if ([contentType isEqualToString:@"audio/x-scpls"] ||
[contentType isEqualToString:@"application/pls+xml"]) {
_format = kFSPlaylistFormatPLS;
} else if ([contentType isEqualToString:@"text/plain"]) {
/* The server did not provide meaningful content type;
last resort: check the file suffix, if there is one */
if ([absoluteUrl hasSuffix:@".m3u"]) {
_format = kFSPlaylistFormatM3U;
} else if ([absoluteUrl hasSuffix:@".pls"]) {
_format = kFSPlaylistFormatPLS;
}
}
if (_format == kFSPlaylistFormatNone) {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSParsePlaylistRequest: Unable to determine the type of the playlist for URL: %@", _url);
#endif
self.onFailure();
} else {
completionHandler(NSURLSessionResponseAllow);
}
[_receivedData setLength:0];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
// Resume the Download Task manually because apparently iOS does not do it automatically?!
[downloadTask resume];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
[_receivedData appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
if(error) {
@synchronized (self) {
_task = nil;
_receivedData = nil;
}
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSParsePlaylistRequest: Connection failed for URL: %@, error %@", _url, [error localizedDescription]);
#endif
self.onFailure();
} else {
@synchronized (self) {
_task = nil;
}
if (_httpStatus != 200) {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSParsePlaylistRequest: Unable to receive playlist from URL: %@", _url);
#endif
self.onFailure();
return;
}
[self parsePlaylistFromData:_receivedData];
self.onCompletion();
}
}
@end

View File

@ -0,0 +1,28 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import "FSXMLHttpRequest.h"
/**
* Use this request for retrieving the contents for a podcast RSS feed.
* Upon request completion, the resulting playlist items are
* in the playlistItems property.
*
* See the FSXMLHttpRequest class how to form a request to retrieve
* the RSS feed.
*/
@interface FSParseRssPodcastFeedRequest : FSXMLHttpRequest {
NSMutableArray *_playlistItems;
}
/**
* The playlist items stored in the FSPlaylistItem class.
*/
@property (readonly) NSMutableArray *playlistItems;
@end

View File

@ -0,0 +1,100 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <libxml/parser.h>
#import <libxml/xpath.h>
#import "FSParseRssPodcastFeedRequest.h"
#import "FSPlaylistItem.h"
static NSString *const kXPathQueryItems = @"/rss/channel/item";
@interface FSParseRssPodcastFeedRequest ()
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl;
- (void)parseItems:(xmlNodePtr)node;
@end
@implementation FSParseRssPodcastFeedRequest
- (NSURL *)parseLocalFileUrl:(NSString *)fileUrl
{
// Resolve the local bundle URL
NSString *path = [fileUrl substringFromIndex:7];
NSRange range = [path rangeOfString:@"." options:NSBackwardsSearch];
NSString *fileName = [path substringWithRange:NSMakeRange(0, range.location)];
NSString *suffix = [path substringWithRange:NSMakeRange(range.location + 1, [path length] - [fileName length] - 1)];
return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:suffix]];
}
- (void)parseItems:(xmlNodePtr)node
{
FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
for (xmlNodePtr n = node->children; n != NULL; n = n->next) {
NSString *nodeName = @((const char *)n->name);
if ([nodeName isEqualToString:@"title"]) {
item.title = [self contentForNode:n];
} else if ([nodeName isEqualToString:@"enclosure"]) {
NSString *url = [self contentForNodeAttribute:n attribute:"url"];
if ([url hasPrefix:@"file://"]) {
item.url = [self parseLocalFileUrl:url];
} else {
item.url = [NSURL URLWithString:url];
}
} else if ([nodeName isEqualToString:@"link"]) {
NSString *url = [self contentForNode:n];
if ([url hasPrefix:@"file://"]) {
item.originatingUrl = [self parseLocalFileUrl:url];
} else {
item.originatingUrl = [NSURL URLWithString:url];
}
}
}
if (nil == item.url &&
nil == item.originatingUrl) {
// Not a valid item, as there is no URL. Skip.
return;
}
[_playlistItems addObject:item];
}
- (void)parseResponseData
{
if (!_playlistItems) {
_playlistItems = [[NSMutableArray alloc] init];
}
[_playlistItems removeAllObjects];
// RSS feed publication date format:
// Sun, 22 Jul 2012 17:35:05 GMT
[_dateFormatter setDateFormat:@"EEE, dd MMMM yyyy HH:mm:ss V"];
[_dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"]];
[self performXPathQuery:kXPathQueryItems];
}
- (void)parseXMLNode:(xmlNodePtr)node xPathQuery:(NSString *)xPathQuery
{
if ([xPathQuery isEqualToString:kXPathQueryItems]) {
[self parseItems:node];
}
}
- (NSArray *)playlistItems
{
return _playlistItems;
}
@end

View File

@ -0,0 +1,41 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <Foundation/Foundation.h>
/**
* A playlist item. Each item has a title and url.
*/
@interface FSPlaylistItem : NSObject {
}
/**
* The title of the playlist item.
*/
@property (nonatomic,copy) NSString *title;
/**
* The URL of the playlist item.
*/
@property (nonatomic,copy) NSURL *url;
/**
* The originating URL of the playlist item.
*/
@property (nonatomic,copy) NSURL *originatingUrl;
/**
* The number of bytes of audio data. Notice that this may differ
* from the number of bytes the server returns for the content length!
* For instance audio file meta data is excluded from the count.
* Effectively you can use this property for seeking calculations.
*
* The property is only available for non-continuous streams which
* have been in the "playing" state.
*/
@property (nonatomic,assign) UInt64 audioDataByteCount;
@end

View File

@ -0,0 +1,25 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import "FSPlaylistItem.h"
@implementation FSPlaylistItem
- (BOOL)isEqual:(id)anObject
{
FSPlaylistItem *otherObject = anObject;
if ([otherObject.title isEqual:self.title] &&
[otherObject.url isEqual:self.url]) {
return YES;
}
return NO;
}
@end

View File

@ -0,0 +1,112 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import <Foundation/Foundation.h>
typedef struct _xmlDoc xmlDoc;
typedef xmlDoc *xmlDocPtr;
typedef struct _xmlNode xmlNode;
typedef xmlNode *xmlNodePtr;
/**
* XML HTTP request error status.
*/
typedef NS_ENUM(NSInteger, FSXMLHttpRequestError) {
/**
* No error.
*/
FSXMLHttpRequestError_NoError = 0,
/**
* Connection failed.
*/
FSXMLHttpRequestError_Connection_Failed,
/**
* Invalid HTTP status.
*/
FSXMLHttpRequestError_Invalid_Http_Status,
/**
* XML parser failed.
*/
FSXMLHttpRequestError_XML_Parser_Failed
};
/**
* FSXMLHttpRequest is a class for retrieving data in the XML
* format over a HTTP or HTTPS connection. It provides
* the necessary foundation for parsing the retrieved XML data.
* This class is not meant to be used directly but subclassed
* to a specific requests.
*
* The usage pattern is the following:
*
* 1. Specify the URL with the url property.
* 2. Define the onCompletion and onFailure handlers.
* 3. Call the start method.
*/
@interface FSXMLHttpRequest : NSObject {
NSURLSessionTask *_task;
xmlDocPtr _xmlDocument;
NSDateFormatter *_dateFormatter;
}
/**
* The URL of the request.
*/
@property (nonatomic,copy) NSURL *url;
/**
* Called upon completion of the request.
*/
@property (copy) void (^onCompletion)(void);
/**
* Called upon a failure.
*/
@property (copy) void (^onFailure)(void);
/**
* If the request fails, contains the latest error status.
*/
@property (readonly) FSXMLHttpRequestError lastError;
/**
* Starts the request.
*/
- (void)start;
/**
* Cancels the request.
*/
- (void)cancel;
/**
* Performs an XPath query on the parsed XML data.
* Yields a parseXMLNode method call, which must be
* defined in the subclasses.
*
* @param query The XPath query to be performed.
*/
- (NSArray *)performXPathQuery:(NSString *)query;
/**
* Retrieves content for the given XML node.
*
* @param node The node for content retreval.
*/
- (NSString *)contentForNode:(xmlNodePtr)node;
/**
* Retrieves content for the given XML node attribute.
*
* @param node The node for content retrieval.
* @param attr The attribute from which the content is retrieved.
*/
- (NSString *)contentForNodeAttribute:(xmlNodePtr)node attribute:(const char *)attr;
/**
* Retrieves date from the given XML node.
*
* @param node The node for retrieving the date.
*/
- (NSDate *)dateFromNode:(xmlNodePtr)node;
@end

View File

@ -0,0 +1,255 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#import "FSXMLHttpRequest.h"
#import <libxml/parser.h>
#import <libxml/xpath.h>
#define DATE_COMPONENTS (NSYearCalendarUnit| NSMonthCalendarUnit | NSDayCalendarUnit | NSWeekCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit | NSWeekdayCalendarUnit | NSWeekdayOrdinalCalendarUnit)
#define CURRENT_CALENDAR [NSCalendar currentCalendar]
@interface FSXMLHttpRequest (PrivateMethods)
- (const char *)detectEncoding;
- (void)parseResponseData;
- (void)parseXMLNode:(xmlNodePtr)node xPathQuery:(NSString *)xPathQuery;
@end
@implementation FSXMLHttpRequest
- (id)init
{
self = [super init];
if (self) {
_dateFormatter = [[NSDateFormatter alloc] init];
}
return self;
}
- (void)start
{
if (_task) {
return;
}
_lastError = FSXMLHttpRequestError_NoError;
NSURLRequest *request = [NSURLRequest requestWithURL:self.url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0];
NSURLSession *session = [NSURLSession sharedSession];
__weak FSXMLHttpRequest *weakSelf = self;
@synchronized (self) {
_task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
FSXMLHttpRequest *strongSelf = weakSelf;
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
if(error) {
strongSelf->_lastError = FSXMLHttpRequestError_Connection_Failed;
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSXMLHttpRequest: Request failed for URL: %@, error %@", strongSelf.url, [error localizedDescription]);
#endif
dispatch_async(dispatch_get_main_queue(), ^(){
strongSelf.onFailure();
});
} else {
if (httpResponse.statusCode != 200) {
strongSelf->_lastError = FSXMLHttpRequestError_Invalid_Http_Status;
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSXMLHttpRequest: Unable to receive content for URL: %@", strongSelf.url);
#endif
dispatch_async(dispatch_get_main_queue(), ^(){
strongSelf.onFailure();
});
return;
}
const char *encoding = [self detectEncoding:data];
strongSelf->_xmlDocument = xmlReadMemory([data bytes],
(int)[data length],
"",
encoding,
0);
if (!strongSelf->_xmlDocument) {
strongSelf->_lastError = FSXMLHttpRequestError_XML_Parser_Failed;
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
NSLog(@"FSXMLHttpRequest: Unable to parse the content for URL: %@", strongSelf.url);
#endif
dispatch_async(dispatch_get_main_queue(), ^(){
strongSelf.onFailure();
});
return;
}
[strongSelf parseResponseData];
xmlFreeDoc(strongSelf->_xmlDocument);
strongSelf->_xmlDocument = nil;
dispatch_async(dispatch_get_main_queue(), ^(){
strongSelf.onCompletion();
});
}
}];
}
[_task resume];
}
- (void)cancel
{
if (!_task) {
return;
}
@synchronized (self) {
[_task cancel];
_task = nil;
}
}
/*
* =======================================
* XML handling
* =======================================
*/
- (NSArray *)performXPathQuery:(NSString *)query
{
NSMutableArray *resultNodes = [NSMutableArray array];
xmlXPathContextPtr xpathCtx = NULL;
xmlXPathObjectPtr xpathObj = NULL;
xpathCtx = xmlXPathNewContext(_xmlDocument);
if (xpathCtx == NULL) {
goto cleanup;
}
xpathObj = xmlXPathEvalExpression((xmlChar *)[query cStringUsingEncoding:NSUTF8StringEncoding], xpathCtx);
if (xpathObj == NULL) {
goto cleanup;
}
xmlNodeSetPtr nodes = xpathObj->nodesetval;
if (!nodes) {
goto cleanup;
}
for (size_t i = 0; i < nodes->nodeNr; i++) {
[self parseXMLNode:nodes->nodeTab[i] xPathQuery:query];
}
cleanup:
if (xpathObj) {
xmlXPathFreeObject(xpathObj);
}
if (xpathCtx) {
xmlXPathFreeContext(xpathCtx);
}
return resultNodes;
}
- (NSString *)contentForNode:(xmlNodePtr)node
{
NSString *stringWithContent;
if (!node) {
stringWithContent = [[NSString alloc] init];
} else {
xmlChar *content = xmlNodeGetContent(node);
if (!content) {
return stringWithContent;
}
stringWithContent = @((const char *)content);
xmlFree(content);
}
return stringWithContent;
}
- (NSString *)contentForNodeAttribute:(xmlNodePtr)node attribute:(const char *)attr
{
NSString *stringWithContent;
if (!node) {
stringWithContent = [[NSString alloc] init];
} else {
xmlChar *content = xmlGetProp(node, (const xmlChar *)attr);
if (!content) {
return stringWithContent;
}
stringWithContent = @((const char *)content);
xmlFree(content);
}
return stringWithContent;
}
/*
* =======================================
* Helpers
* =======================================
*/
- (const char *)detectEncoding:(NSData *)receivedData
{
const char *encoding = 0;
const char *header = strndup([receivedData bytes], 60);
if (strstr(header, "utf-8") || strstr(header, "UTF-8")) {
encoding = "UTF-8";
} else if (strstr(header, "iso-8859-1") || strstr(header, "ISO-8859-1")) {
encoding = "ISO-8859-1";
}
free((void *)header);
return encoding;
}
- (NSDate *)dateFromNode:(xmlNodePtr)node
{
NSString *dateString = [self contentForNode:node];
/*
* For some NSDateFormatter date parsing oddities: http://www.openradar.me/9944011
*
* Engineering has determined that this issue behaves as intended based on the following information:
*
* This is an intentional change in iOS 5. The issue is this: With the short formats as specified by z (=zzz) or v (=vvv),
* there can be a lot of ambiguity. For example, "ET" for Eastern Time" could apply to different time zones in many different regions.
* To improve formatting and parsing reliability, the short forms are only used in a locale if the "cu" (commonly used) flag is set
* for the locale. Otherwise, only the long forms are used (for both formatting and parsing). This is a change in
* open-source CLDR 2.0 / ICU 4.8, which is the basis for the ICU in iOS 5, which in turn is the basis of NSDateFormatter behavior.
*
* For the "en" locale (= "en_US"), the cu flag is set for metazones such as Alaska, America_Central, America_Eastern, America_Mountain,
* America_Pacific, Atlantic, Hawaii_Aleutian, and GMT. It is not set for Europe_Central.
*
* However, for the "en_GB" locale, the cu flag is set for Europe_Central.
*
* So, a formatter set for short timezone style "z" or "zzz" and locale "en" or "en_US" will not parse "CEST" or "CET", but if the
* locale is instead set to "en_GB" it will parse those. The "GMT" style will be parsed by all.
*
* If the formatter is set for the long timezone style "zzzz", and the locale is any of "en", "en_US", or "en_GB", then any of the
* following will be parsed, because they are unambiguous:
*
* "Pacific Daylight Time" "Central European Summer Time" "Central European Time"
*
*/
return [_dateFormatter dateFromString:dateString];
}
@end

View File

@ -0,0 +1,566 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#include "audio_queue.h"
#include "stream_configuration.h"
#include <pthread.h>
//#define AQ_DEBUG 1
//#define AQ_DEBUG_LOCKS 1
#if !defined (AQ_DEBUG)
#define AQ_TRACE(...) do {} while (0)
#define AQ_ASSERT(...) do {} while (0)
#else
#include <cassert>
#define AQ_TRACE(...) printf(__VA_ARGS__)
#define AQ_ASSERT(...) assert(__VA_ARGS__)
#endif
#if !defined (AQ_DEBUG_LOCKS)
#define AQ_LOCK_TRACE(...) do {} while (0)
#else
#define AQ_LOCK_TRACE(...) printf(__VA_ARGS__)
#endif
namespace astreamer {
/* public */
Audio_Queue::Audio_Queue()
: m_delegate(0),
m_state(IDLE),
m_outAQ(0),
m_fillBufferIndex(0),
m_bytesFilled(0),
m_packetsFilled(0),
m_buffersUsed(0),
m_audioQueueStarted(false),
m_levelMeteringEnabled(false),
m_lastError(noErr),
m_initialOutputVolume(1.0)
{
Stream_Configuration *config = Stream_Configuration::configuration();
m_audioQueueBuffer = new AudioQueueBufferRef[config->bufferCount];
m_packetDescs = new AudioStreamPacketDescription[config->maxPacketDescs];
m_bufferInUse = new bool[config->bufferCount];
for (size_t i=0; i < config->bufferCount; i++) {
m_bufferInUse[i] = false;
}
if (pthread_mutex_init(&m_mutex, NULL) != 0) {
AQ_TRACE("m_mutex init failed!\n");
}
if (pthread_mutex_init(&m_bufferInUseMutex, NULL) != 0) {
AQ_TRACE("m_bufferInUseMutex init failed!\n");
}
if (pthread_cond_init(&m_bufferFreeCondition, NULL) != 0) {
AQ_TRACE("m_bufferFreeCondition init failed!\n");
}
}
Audio_Queue::~Audio_Queue()
{
stop(true);
cleanup();
delete [] m_audioQueueBuffer;
delete [] m_packetDescs;
delete [] m_bufferInUse;
pthread_mutex_destroy(&m_mutex);
pthread_mutex_destroy(&m_bufferInUseMutex);
pthread_cond_destroy(&m_bufferFreeCondition);
}
bool Audio_Queue::initialized()
{
return (m_outAQ != 0);
}
void Audio_Queue::start()
{
// start the queue if it has not been started already
if (m_audioQueueStarted) {
return;
}
OSStatus err = AudioQueueStart(m_outAQ, NULL);
if (!err) {
m_audioQueueStarted = true;
m_levelMeteringEnabled = false;
m_lastError = noErr;
} else {
AQ_TRACE("%s: AudioQueueStart failed!\n", __PRETTY_FUNCTION__);
m_lastError = err;
}
}
void Audio_Queue::pause()
{
if (m_state == RUNNING) {
if (AudioQueuePause(m_outAQ) != 0) {
AQ_TRACE("%s: AudioQueuePause failed!\n", __PRETTY_FUNCTION__);
}
setState(PAUSED);
} else if (m_state == PAUSED) {
AudioQueueStart(m_outAQ, NULL);
setState(RUNNING);
}
}
void Audio_Queue::stop()
{
stop(true);
}
float Audio_Queue::volume()
{
if (!m_outAQ) {
return 1.0;
}
float vol;
OSStatus err = AudioQueueGetParameter(m_outAQ, kAudioQueueParam_Volume, &vol);
if (!err) {
return vol;
}
return 1.0;
}
void Audio_Queue::setVolume(float volume)
{
if (!m_outAQ) {
return;
}
AudioQueueSetParameter(m_outAQ, kAudioQueueParam_Volume, volume);
}
void Audio_Queue::setPlayRate(float playRate)
{
Stream_Configuration *configuration = Stream_Configuration::configuration();
if (!configuration->enableTimeAndPitchConversion) {
#if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
printf("*** FreeStreamer notification: Trying to set play rate for audio queue but enableTimeAndPitchConversion is disabled from configuration. Play rate settign will not work.\n");
#endif
return;
}
if (!m_outAQ) {
return;
}
if (playRate < 0.5) {
playRate = 0.5;
}
if (playRate > 2.0) {
playRate = 2.0;
}
AudioQueueSetParameter(m_outAQ, kAudioQueueParam_PlayRate, playRate);
}
void Audio_Queue::stop(bool stopImmediately)
{
if (!m_audioQueueStarted) {
AQ_TRACE("%s: audio queue already stopped, return!\n", __PRETTY_FUNCTION__);
return;
}
m_audioQueueStarted = false;
m_levelMeteringEnabled = false;
pthread_mutex_lock(&m_bufferInUseMutex);
pthread_cond_signal(&m_bufferFreeCondition);
pthread_mutex_unlock(&m_bufferInUseMutex);
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
if (AudioQueueFlush(m_outAQ) != 0) {
AQ_TRACE("%s: AudioQueueFlush failed!\n", __PRETTY_FUNCTION__);
}
if (stopImmediately) {
AudioQueueRemovePropertyListener(m_outAQ,
kAudioQueueProperty_IsRunning,
audioQueueIsRunningCallback,
this);
}
if (AudioQueueStop(m_outAQ, stopImmediately) != 0) {
AQ_TRACE("%s: AudioQueueStop failed!\n", __PRETTY_FUNCTION__);
}
if (stopImmediately) {
setState(IDLE);
}
AQ_TRACE("%s: leave\n", __PRETTY_FUNCTION__);
}
AudioTimeStamp Audio_Queue::currentTime()
{
AudioTimeStamp queueTime;
Boolean discontinuity;
memset(&queueTime, 0, sizeof queueTime);
OSStatus err = AudioQueueGetCurrentTime(m_outAQ, NULL, &queueTime, &discontinuity);
if (err) {
AQ_TRACE("AudioQueueGetCurrentTime failed\n");
}
return queueTime;
}
AudioQueueLevelMeterState Audio_Queue::levels()
{
if (!m_levelMeteringEnabled) {
UInt32 enabledLevelMeter = true;
AudioQueueSetProperty(m_outAQ,
kAudioQueueProperty_EnableLevelMetering,
&enabledLevelMeter,
sizeof(UInt32));
m_levelMeteringEnabled = true;
}
AudioQueueLevelMeterState levelMeter;
UInt32 levelMeterSize = sizeof(AudioQueueLevelMeterState);
AudioQueueGetProperty(m_outAQ, kAudioQueueProperty_CurrentLevelMeterDB, &levelMeter, &levelMeterSize);
return levelMeter;
}
void Audio_Queue::init()
{
OSStatus err = noErr;
cleanup();
// create the audio queue
err = AudioQueueNewOutput(&m_streamDesc, audioQueueOutputCallback, this, CFRunLoopGetCurrent(), NULL, 0, &m_outAQ);
if (err) {
AQ_TRACE("%s: error in AudioQueueNewOutput\n", __PRETTY_FUNCTION__);
m_lastError = err;
if (m_delegate) {
m_delegate->audioQueueInitializationFailed();
}
return;
}
Stream_Configuration *configuration = Stream_Configuration::configuration();
// allocate audio queue buffers
for (unsigned int i = 0; i < configuration->bufferCount; ++i) {
err = AudioQueueAllocateBuffer(m_outAQ, configuration->bufferSize, &m_audioQueueBuffer[i]);
if (err) {
/* If allocating the buffers failed, everything else will fail, too.
* Dispose the queue so that we can later on detect that this
* queue in fact has not been initialized.
*/
AQ_TRACE("%s: error in AudioQueueAllocateBuffer\n", __PRETTY_FUNCTION__);
(void)AudioQueueDispose(m_outAQ, true);
m_outAQ = 0;
m_lastError = err;
if (m_delegate) {
m_delegate->audioQueueInitializationFailed();
}
return;
}
}
// listen for kAudioQueueProperty_IsRunning
err = AudioQueueAddPropertyListener(m_outAQ, kAudioQueueProperty_IsRunning, audioQueueIsRunningCallback, this);
if (err) {
AQ_TRACE("%s: error in AudioQueueAddPropertyListener\n", __PRETTY_FUNCTION__);
m_lastError = err;
return;
}
if (configuration->enableTimeAndPitchConversion) {
UInt32 enableTimePitchConversion = 1;
err = AudioQueueSetProperty (m_outAQ, kAudioQueueProperty_EnableTimePitch, &enableTimePitchConversion, sizeof(enableTimePitchConversion));
if (err != noErr) {
AQ_TRACE("Failed to enable time and pitch conversion. Play rate setting will fail\n");
}
}
if (m_initialOutputVolume != 1.0) {
setVolume(m_initialOutputVolume);
}
}
void Audio_Queue::handleAudioPackets(UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions)
{
if (!initialized()) {
AQ_TRACE("%s: warning: attempt to handle audio packets with uninitialized audio queue. return.\n", __PRETTY_FUNCTION__);
return;
}
// this is called by audio file stream when it finds packets of audio
AQ_TRACE("got data. bytes: %u packets: %u\n", inNumberBytes, (unsigned int)inNumberPackets);
/* Place each packet into a buffer and then send each buffer into the audio
queue */
UInt32 i;
for (i = 0; i < inNumberPackets; i++) {
AudioStreamPacketDescription *desc = &inPacketDescriptions[i];
const void *data = (const char*)inInputData + desc->mStartOffset;
if (!initialized()) {
AQ_TRACE("%s: warning: attempt to handle audio packets with uninitialized audio queue. return.\n", __PRETTY_FUNCTION__);
return;
}
Stream_Configuration *config = Stream_Configuration::configuration();
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
UInt32 packetSize = desc->mDataByteSize;
/* This shouldn't happen because most of the time we read the packet buffer
size from the file stream, but if we restored to guessing it we could
come up too small here */
if (packetSize > config->bufferSize) {
AQ_TRACE("%s: packetSize %u > AQ_BUFSIZ %li\n", __PRETTY_FUNCTION__, (unsigned int)packetSize, config->bufferSize);
return;
}
// if the space remaining in the buffer is not enough for this packet, then
// enqueue the buffer and wait for another to become available.
if (config->bufferSize - m_bytesFilled < packetSize) {
enqueueBuffer();
if (!m_audioQueueStarted) {
return;
}
} else {
AQ_TRACE("%s: skipped enqueueBuffer AQ_BUFSIZ - m_bytesFilled %lu, packetSize %u\n", __PRETTY_FUNCTION__, (config->bufferSize - m_bytesFilled), (unsigned int)packetSize);
}
// copy data to the audio queue buffer
AudioQueueBufferRef buf = m_audioQueueBuffer[m_fillBufferIndex];
memcpy((char*)buf->mAudioData, data, packetSize);
// fill out packet description to pass to enqueue() later on
m_packetDescs[m_packetsFilled] = *desc;
// Make sure the offset is relative to the start of the audio buffer
m_packetDescs[m_packetsFilled].mStartOffset = m_bytesFilled;
// keep track of bytes filled and packets filled
m_bytesFilled += packetSize;
m_packetsFilled++;
/* If filled our buffer with packets, then commit it to the system */
if (m_packetsFilled >= config->maxPacketDescs) {
enqueueBuffer();
}
}
}
/* private */
void Audio_Queue::cleanup()
{
if (!initialized()) {
AQ_TRACE("%s: warning: attempt to cleanup an uninitialized audio queue. return.\n", __PRETTY_FUNCTION__);
return;
}
Stream_Configuration *config = Stream_Configuration::configuration();
if (m_state != IDLE) {
AQ_TRACE("%s: attemping to cleanup the audio queue when it is still playing, force stopping\n",
__PRETTY_FUNCTION__);
AudioQueueRemovePropertyListener(m_outAQ,
kAudioQueueProperty_IsRunning,
audioQueueIsRunningCallback,
this);
AudioQueueStop(m_outAQ, true);
setState(IDLE);
}
if (AudioQueueDispose(m_outAQ, true) != 0) {
AQ_TRACE("%s: AudioQueueDispose failed!\n", __PRETTY_FUNCTION__);
}
m_outAQ = 0;
m_fillBufferIndex = m_bytesFilled = m_packetsFilled = m_buffersUsed = 0;
for (size_t i=0; i < config->bufferCount; i++) {
m_bufferInUse[i] = false;
}
m_lastError = noErr;
}
void Audio_Queue::setState(State state)
{
if (m_state == state) {
/* We are already in this state! */
return;
}
m_state = state;
if (m_delegate) {
m_delegate->audioQueueStateChanged(state);
}
}
void Audio_Queue::enqueueBuffer()
{
AQ_ASSERT(!m_bufferInUse[m_fillBufferIndex]);
Stream_Configuration *config = Stream_Configuration::configuration();
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
pthread_mutex_lock(&m_bufferInUseMutex);
m_bufferInUse[m_fillBufferIndex] = true;
m_buffersUsed++;
// enqueue buffer
AudioQueueBufferRef fillBuf = m_audioQueueBuffer[m_fillBufferIndex];
fillBuf->mAudioDataByteSize = m_bytesFilled;
pthread_mutex_unlock(&m_bufferInUseMutex);
AQ_ASSERT(m_packetsFilled > 0);
OSStatus err = AudioQueueEnqueueBuffer(m_outAQ, fillBuf, m_packetsFilled, m_packetDescs);
if (!err) {
m_lastError = noErr;
start();
} else {
/* If we get an error here, it very likely means that the audio queue is no longer
running */
AQ_TRACE("%s: error in AudioQueueEnqueueBuffer\n", __PRETTY_FUNCTION__);
m_lastError = err;
return;
}
pthread_mutex_lock(&m_bufferInUseMutex);
// go to next buffer
if (++m_fillBufferIndex >= config->bufferCount) {
m_fillBufferIndex = 0;
}
// reset bytes filled
m_bytesFilled = 0;
// reset packets filled
m_packetsFilled = 0;
// wait until next buffer is not in use
while (m_bufferInUse[m_fillBufferIndex]) {
AQ_TRACE("waiting for buffer %u\n", (unsigned int)m_fillBufferIndex);
pthread_cond_wait(&m_bufferFreeCondition, &m_bufferInUseMutex);
}
pthread_mutex_unlock(&m_bufferInUseMutex);
}
// this is called by the audio queue when it has finished decoding our data.
// The buffer is now free to be reused.
void Audio_Queue::audioQueueOutputCallback(void *inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
{
Audio_Queue *audioQueue = static_cast<Audio_Queue*>(inClientData);
Stream_Configuration *config = Stream_Configuration::configuration();
int bufIndex = -1;
for (unsigned int i = 0; i < config->bufferCount; ++i) {
if (inBuffer == audioQueue->m_audioQueueBuffer[i]) {
AQ_TRACE("findQueueBuffer %i\n", i);
bufIndex = i;
break;
}
}
if (bufIndex == -1) {
return;
}
pthread_mutex_lock(&audioQueue->m_bufferInUseMutex);
AQ_ASSERT(audioQueue->m_bufferInUse[bufIndex]);
audioQueue->m_bufferInUse[bufIndex] = false;
audioQueue->m_buffersUsed--;
AQ_TRACE("signaling buffer free for inuse %i....\n", bufIndex);
pthread_cond_signal(&audioQueue->m_bufferFreeCondition);
AQ_TRACE("signal sent!\n");
if (audioQueue->m_buffersUsed == 0 && audioQueue->m_delegate) {
AQ_LOCK_TRACE("audioQueueOutputCallback: unlock 2\n");
pthread_mutex_unlock(&audioQueue->m_bufferInUseMutex);
if (audioQueue->m_delegate) {
audioQueue->m_delegate->audioQueueBuffersEmpty();
}
} else {
pthread_mutex_unlock(&audioQueue->m_bufferInUseMutex);
if (audioQueue->m_delegate) {
audioQueue->m_delegate->audioQueueFinishedPlayingPacket();
}
}
AQ_LOCK_TRACE("audioQueueOutputCallback: unlock\n");
}
void Audio_Queue::audioQueueIsRunningCallback(void *inClientData, AudioQueueRef inAQ, AudioQueuePropertyID inID)
{
Audio_Queue *audioQueue = static_cast<Audio_Queue*>(inClientData);
AQ_TRACE("%s: enter\n", __PRETTY_FUNCTION__);
UInt32 running;
UInt32 output = sizeof(running);
OSStatus err = AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &running, &output);
if (err) {
AQ_TRACE("%s: error in kAudioQueueProperty_IsRunning\n", __PRETTY_FUNCTION__);
return;
}
if (running) {
AQ_TRACE("audio queue running!\n");
audioQueue->setState(RUNNING);
} else {
audioQueue->setState(IDLE);
}
}
} // namespace astreamer

View File

@ -0,0 +1,102 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#ifndef ASTREAMER_AUDIO_QUEUE_H
#define ASTREAMER_AUDIO_QUEUE_H
#include <AudioToolbox/AudioToolbox.h> /* AudioFileStreamID */
namespace astreamer {
class Audio_Queue_Delegate;
struct queued_packet;
class Audio_Queue {
public:
Audio_Queue_Delegate *m_delegate;
enum State {
IDLE,
RUNNING,
PAUSED
};
Audio_Queue();
virtual ~Audio_Queue();
bool initialized();
void init();
// Notice: the queue blocks if it has no free buffers
void handleAudioPackets(UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions);
void start();
void pause();
void stop(bool stopImmediately);
void stop();
float volume();
void setVolume(float volume);
void setPlayRate(float playRate);
AudioTimeStamp currentTime();
AudioQueueLevelMeterState levels();
private:
Audio_Queue(const Audio_Queue&);
Audio_Queue& operator=(const Audio_Queue&);
State m_state;
AudioQueueRef m_outAQ; // the audio queue
AudioQueueBufferRef *m_audioQueueBuffer; // audio queue buffers
AudioStreamPacketDescription *m_packetDescs; // packet descriptions for enqueuing audio
UInt32 m_fillBufferIndex; // the index of the audioQueueBuffer that is being filled
UInt32 m_bytesFilled; // how many bytes have been filled
UInt32 m_packetsFilled; // how many packets have been filled
UInt32 m_buffersUsed; // how many buffers are used
bool m_audioQueueStarted; // flag to indicate that the queue has been started
bool *m_bufferInUse; // flags to indicate that a buffer is still in use
bool m_levelMeteringEnabled;
pthread_mutex_t m_mutex;
pthread_mutex_t m_bufferInUseMutex;
pthread_cond_t m_bufferFreeCondition;
public:
OSStatus m_lastError;
AudioStreamBasicDescription m_streamDesc;
float m_initialOutputVolume;
private:
void cleanup();
void setCookiesForStream(AudioFileStreamID inAudioFileStream);
void setState(State state);
void enqueueBuffer();
static void audioQueueOutputCallback(void *inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);
static void audioQueueIsRunningCallback(void *inClientData, AudioQueueRef inAQ, AudioQueuePropertyID inID);
};
class Audio_Queue_Delegate {
public:
virtual void audioQueueStateChanged(Audio_Queue::State state) = 0;
virtual void audioQueueBuffersEmpty() = 0;
virtual void audioQueueInitializationFailed() = 0;
virtual void audioQueueFinishedPlayingPacket() = 0;
};
} // namespace astreamer
#endif // ASTREAMER_AUDIO_QUEUE_H

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,255 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#ifndef ASTREAMER_AUDIO_STREAM_H
#define ASTREAMER_AUDIO_STREAM_H
#import "input_stream.h"
#include "audio_queue.h"
#include <AudioToolbox/AudioToolbox.h>
#include <list>
namespace astreamer {
typedef struct queued_packet {
UInt64 identifier;
AudioStreamPacketDescription desc;
struct queued_packet *next;
char data[];
} queued_packet_t;
typedef struct {
float offset;
float timePlayed;
} AS_Playback_Position;
enum Audio_Stream_Error {
AS_ERR_OPEN = 1, // Cannot open the audio stream
AS_ERR_STREAM_PARSE = 2, // Parse error
AS_ERR_NETWORK = 3, // Network error
AS_ERR_UNSUPPORTED_FORMAT = 4,
AS_ERR_BOUNCING = 5,
AS_ERR_TERMINATED = 6
};
class Audio_Stream_Delegate;
class File_Output;
#define kAudioStreamBitrateBufferSize 50
class Audio_Stream : public Input_Stream_Delegate, public Audio_Queue_Delegate {
public:
Audio_Stream_Delegate *m_delegate;
enum State {
STOPPED,
BUFFERING,
PLAYING,
PAUSED,
SEEKING,
FAILED,
END_OF_FILE,
PLAYBACK_COMPLETED
};
Audio_Stream();
virtual ~Audio_Stream();
void open();
void open(Input_Stream_Position *position);
void close(bool closeParser);
void pause();
void rewind(unsigned seconds);
void startCachedDataPlayback();
AS_Playback_Position playbackPosition();
UInt64 audioDataByteCount();
float durationInSeconds();
void seekToOffset(float offset);
Input_Stream_Position streamPositionForOffset(float offset);
float currentVolume();
void setDecoderRunState(bool decoderShouldRun);
void setVolume(float volume);
void setPlayRate(float playRate);
void setUrl(CFURLRef url);
void setStrictContentTypeChecking(bool strictChecking);
void setDefaultContentType(CFStringRef defaultContentType);
void setSeekOffset(float offset);
void setDefaultContentLength(UInt64 defaultContentLength);
void setContentLength(UInt64 contentLength);
void setPreloading(bool preloading);
bool isPreloading();
void setOutputFile(CFURLRef url);
CFURLRef outputFile();
State state();
CFStringRef sourceFormatDescription();
CFStringRef contentType();
CFStringRef createCacheIdentifierForURL(CFURLRef url);
size_t cachedDataSize();
bool strictContentTypeChecking();
float bitrate();
UInt64 defaultContentLength();
UInt64 contentLength();
int playbackDataCount();
AudioQueueLevelMeterState levels();
/* Audio_Queue_Delegate */
void audioQueueStateChanged(Audio_Queue::State state);
void audioQueueBuffersEmpty();
void audioQueueInitializationFailed();
void audioQueueFinishedPlayingPacket();
/* Input_Stream_Delegate */
void streamIsReadyRead();
void streamHasBytesAvailable(UInt8 *data, UInt32 numBytes);
void streamEndEncountered();
void streamErrorOccurred(CFStringRef errorDesc);
void streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
void streamMetaDataByteSizeAvailable(UInt32 sizeInBytes);
private:
Audio_Stream(const Audio_Stream&);
Audio_Stream& operator=(const Audio_Stream&);
bool m_inputStreamRunning;
bool m_audioStreamParserRunning;
bool m_initialBufferingCompleted;
bool m_discontinuity;
bool m_preloading;
bool m_audioQueueConsumedPackets;
UInt64 m_defaultContentLength;
UInt64 m_contentLength;
UInt64 m_originalContentLength;
UInt64 m_bytesReceived;
State m_state;
Input_Stream *m_inputStream;
Audio_Queue *m_audioQueue;
CFRunLoopTimerRef m_watchdogTimer;
CFRunLoopTimerRef m_seekTimer;
CFRunLoopTimerRef m_inputStreamTimer;
CFRunLoopTimerRef m_stateSetTimer;
CFRunLoopTimerRef m_decodeTimer;
AudioFileStreamID m_audioFileStream; // the audio file stream parser
AudioConverterRef m_audioConverter;
AudioStreamBasicDescription m_srcFormat;
AudioStreamBasicDescription m_dstFormat;
OSStatus m_initializationError;
UInt32 m_outputBufferSize;
UInt8 *m_outputBuffer;
UInt64 m_packetIdentifier;
UInt64 m_playingPacketIdentifier;
UInt64 m_dataOffset;
float m_seekOffset;
size_t m_bounceCount;
CFAbsoluteTime m_firstBufferingTime;
bool m_strictContentTypeChecking;
CFStringRef m_defaultContentType;
CFStringRef m_contentType;
File_Output *m_fileOutput;
CFURLRef m_outputFile;
queued_packet_t *m_queuedHead;
queued_packet_t *m_queuedTail;
queued_packet_t *m_playPacket;
std::list <queued_packet_t*> m_processedPackets;
unsigned m_numPacketsToRewind;
size_t m_cachedDataSize;
UInt64 m_audioDataByteCount;
UInt64 m_audioDataPacketCount;
UInt32 m_bitRate;
UInt32 m_metaDataSizeInBytes;
double m_packetDuration;
double m_bitrateBuffer[kAudioStreamBitrateBufferSize];
size_t m_bitrateBufferIndex;
float m_outputVolume;
bool m_converterRunOutOfData;
bool m_decoderShouldRun;
bool m_decoderFailed;
bool m_decoderThreadCreated;
pthread_mutex_t m_packetQueueMutex;
pthread_mutex_t m_streamStateMutex;
pthread_t m_decodeThread;
CFRunLoopRef m_decodeRunLoop;
CFRunLoopRef m_mainRunLoop;
CFStringRef createHashForString(CFStringRef str);
Audio_Queue *audioQueue();
void closeAudioQueue();
void closeAndSignalError(int error, CFStringRef errorDescription);
void setState(State state);
void setCookiesForStream(AudioFileStreamID inAudioFileStream);
void createWatchdogTimer();
void invalidateWatchdogTimer();
int cachedDataCount();
void determineBufferingLimits();
void cleanupCachedData();
static void watchdogTimerCallback(CFRunLoopTimerRef timer, void *info);
static void seekTimerCallback(CFRunLoopTimerRef timer, void *info);
static void inputStreamTimerCallback(CFRunLoopTimerRef timer, void *info);
static void stateSetTimerCallback(CFRunLoopTimerRef timer, void *info);
bool decoderShouldRun();
static void decodeSinglePacket(CFRunLoopTimerRef timer, void *info);
static void *decodeLoop(void *arg);
static OSStatus encoderDataCallback(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData);
static void propertyValueCallback(void *inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *ioFlags);
static void streamDataCallback(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions);
AudioFileTypeID audioStreamTypeFromContentType(CFStringRef contentType);
};
class Audio_Stream_Delegate {
public:
virtual void audioStreamStateChanged(Audio_Stream::State state) = 0;
virtual void audioStreamErrorOccurred(int errorCode, CFStringRef errorDescription) = 0;
virtual void audioStreamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData) = 0;
virtual void samplesAvailable(AudioBufferList *samples, UInt32 frames, AudioStreamPacketDescription description) = 0;
virtual void bitrateAvailable() = 0;
};
} // namespace astreamer
#endif // ASTREAMER_AUDIO_STREAM_H

View File

@ -0,0 +1,440 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#include "caching_stream.h"
#include "file_output.h"
#include "stream_configuration.h"
#include "file_stream.h"
//#define CS_DEBUG 1
#if !defined (CS_DEBUG)
#define CS_TRACE(...) do {} while (0)
#define CS_TRACE_CFSTRING(X) do {} while (0)
#define CS_TRACE_CFURL(X) do {} while (0)
#else
#define CS_TRACE(...) printf(__VA_ARGS__)
#define CS_TRACE_CFSTRING(X) CS_TRACE("%s\n", CFStringGetCStringPtr(X, kCFStringEncodingMacRoman))
#define CS_TRACE_CFURL(X) CS_TRACE_CFSTRING(CFURLGetString(X))
#endif
namespace astreamer {
Caching_Stream::Caching_Stream(Input_Stream *target) :
m_target(target),
m_fileOutput(0),
m_fileStream(new File_Stream()),
m_cacheable(false),
m_writable(false),
m_useCache(false),
m_cacheMetaDataWritten(false),
m_cacheIdentifier(0),
m_fileUrl(0),
m_metaDataUrl(0)
{
m_target->m_delegate = this;
m_fileStream->m_delegate = this;
}
Caching_Stream::~Caching_Stream()
{
if (m_target) {
delete m_target;
m_target = 0;
}
if (m_fileOutput) {
delete m_fileOutput;
m_fileOutput = 0;
}
if (m_fileStream) {
delete m_fileStream;
m_fileStream = 0;
}
if (m_cacheIdentifier) {
CFRelease(m_cacheIdentifier);
m_cacheIdentifier = 0;
}
if (m_fileUrl) {
CFRelease(m_fileUrl);
m_fileUrl = 0;
}
if (m_metaDataUrl) {
CFRelease(m_metaDataUrl);
m_fileUrl = 0;
}
}
CFURLRef Caching_Stream::createFileURLWithPath(CFStringRef path)
{
CFURLRef fileUrl = NULL;
if (!path) {
return fileUrl;
}
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, path, NULL, NULL, kCFStringEncodingUTF8);
CFURLRef regularUrl = CFURLCreateWithString(kCFAllocatorDefault, (escapedPath ? escapedPath : path), NULL);
if (regularUrl) {
fileUrl = CFURLCreateFilePathURL(kCFAllocatorDefault, regularUrl, NULL);
CFRelease(regularUrl);
}
if (escapedPath) {
CFRelease(escapedPath);
}
return fileUrl;
}
void Caching_Stream::readMetaData()
{
if (!m_metaDataUrl) {
return;
}
CFReadStreamRef readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, m_metaDataUrl);
if (readStream) {
if (CFReadStreamOpen(readStream)) {
UInt8 buf[1024];
CFIndex bytesRead = CFReadStreamRead(readStream, buf, 1024);
if (bytesRead > 0) {
CFStringRef contentType = CFStringCreateWithBytes(kCFAllocatorDefault, buf, bytesRead, kCFStringEncodingUTF8, false);
if (contentType) {
if (m_fileStream) {
CS_TRACE("Setting the content type of the file stream based on the meta data\n");
CS_TRACE_CFSTRING(contentType);
m_fileStream->setContentType(contentType);
}
CFRelease(contentType);
}
}
CFReadStreamClose(readStream);
}
CFRelease(readStream);
}
}
Input_Stream_Position Caching_Stream::position()
{
if (m_useCache) {
return m_fileStream->position();
} else {
return m_target->position();
}
}
CFStringRef Caching_Stream::contentType()
{
if (m_useCache) {
return m_fileStream->contentType();
} else {
return m_target->contentType();
}
}
size_t Caching_Stream::contentLength()
{
if (m_useCache) {
return m_fileStream->contentLength();
} else {
return m_target->contentLength();
}
}
bool Caching_Stream::open()
{
bool status;
if (CFURLResourceIsReachable(m_metaDataUrl, NULL) &&
CFURLResourceIsReachable(m_fileUrl, NULL)) {
m_cacheable = false;
m_writable = false;
m_useCache = true;
m_cacheMetaDataWritten = false;
readMetaData();
CS_TRACE("Playing file from cache\n");
CS_TRACE_CFURL(m_fileUrl);
status = m_fileStream->open();
} else {
m_cacheable = true;
m_writable = false;
m_useCache = false;
m_cacheMetaDataWritten = false;
CS_TRACE("File not cached\n");
status = m_target->open();
}
return status;
}
bool Caching_Stream::open(const Input_Stream_Position& position)
{
bool status;
if (CFURLResourceIsReachable(m_metaDataUrl, NULL) &&
CFURLResourceIsReachable(m_fileUrl, NULL)) {
m_cacheable = false;
m_writable = false;
m_useCache = true;
m_cacheMetaDataWritten = false;
readMetaData();
CS_TRACE("Playing file from cache\n");
CS_TRACE_CFURL(m_fileUrl);
status = m_fileStream->open(position);
} else {
m_cacheable = false;
m_writable = false;
m_useCache = false;
m_cacheMetaDataWritten = false;
CS_TRACE("File not cached\n");
status = m_target->open(position);
}
return status;
}
void Caching_Stream::close()
{
m_fileStream->close();
m_target->close();
}
void Caching_Stream::setScheduledInRunLoop(bool scheduledInRunLoop)
{
if (m_useCache) {
m_fileStream->setScheduledInRunLoop(scheduledInRunLoop);
} else {
m_target->setScheduledInRunLoop(scheduledInRunLoop);
}
}
void Caching_Stream::setUrl(CFURLRef url)
{
m_target->setUrl(url);
}
void Caching_Stream::setCacheIdentifier(CFStringRef cacheIdentifier)
{
m_cacheIdentifier = CFStringCreateCopy(kCFAllocatorDefault, cacheIdentifier);
if (m_fileOutput) {
delete m_fileOutput;
m_fileOutput = 0;
}
Stream_Configuration *config = Stream_Configuration::configuration();
CFStringRef filePath = CFStringCreateWithFormat(NULL, NULL, CFSTR("file://%@/%@"), config->cacheDirectory, m_cacheIdentifier);
CFStringRef metaDataPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("file://%@/%@.metadata"), config->cacheDirectory, m_cacheIdentifier);
if (m_fileUrl) {
CFRelease(m_fileUrl);
m_fileUrl = 0;
}
if (m_metaDataUrl) {
CFRelease(m_metaDataUrl);
m_metaDataUrl = 0;
}
m_fileUrl = createFileURLWithPath(filePath);
m_metaDataUrl = createFileURLWithPath(metaDataPath);
m_fileStream->setUrl(m_fileUrl);
CFRelease(filePath);
CFRelease(metaDataPath);
}
bool Caching_Stream::canHandleUrl(CFURLRef url)
{
if (!url) {
return false;
}
CFStringRef scheme = CFURLCopyScheme(url);
if (scheme) {
if (CFStringCompare(scheme, CFSTR("http"), 0) == kCFCompareEqualTo) {
CFRelease(scheme);
// Using cache makes only sense for HTTP
return true;
}
CFRelease(scheme);
}
// Nothing else to server
return false;
}
/* ID3_Parser_Delegate */
void Caching_Stream::id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
{
if (m_delegate) {
m_delegate->streamMetaDataAvailable(metaData);
}
}
void Caching_Stream::id3tagSizeAvailable(UInt32 tagSize)
{
if (m_delegate) {
m_delegate->streamMetaDataByteSizeAvailable(tagSize);
}
}
/* Input_Stream_Delegate */
void Caching_Stream::streamIsReadyRead()
{
if (m_cacheable) {
// If the stream is cacheable (not seeked from some position)
// Check if the stream has a length. If there is no length,
// it is a continuous stream and thus cannot be cached.
m_cacheable = (m_target->contentLength() > 0);
}
#if CS_DEBUG
if (m_cacheable) CS_TRACE("Stream can be cached!\n");
else CS_TRACE("Stream cannot be cached\n");
#endif
if (m_delegate) {
m_delegate->streamIsReadyRead();
}
}
void Caching_Stream::streamHasBytesAvailable(UInt8 *data, UInt32 numBytes)
{
if (m_cacheable) {
if (numBytes > 0) {
if (!m_fileOutput) {
if (m_fileUrl) {
CS_TRACE("Caching started for stream\n");
m_fileOutput = new File_Output(m_fileUrl);
m_writable = true;
}
}
if (m_writable && m_fileOutput) {
m_writable &= (m_fileOutput->write(data, numBytes) > 0);
}
}
}
if (m_delegate) {
m_delegate->streamHasBytesAvailable(data, numBytes);
}
}
void Caching_Stream::streamEndEncountered()
{
if (m_fileOutput) {
delete m_fileOutput;
m_fileOutput = 0;
}
if (m_cacheable) {
if (m_writable) {
CS_TRACE("Successfully cached the stream\n");
CS_TRACE_CFURL(m_fileUrl);
// We only write the meta data if the stream was successfully streamed.
// In that way we can use the meta data as an indicator that there is a file to stream.
if (!m_cacheMetaDataWritten) {
CFWriteStreamRef writeStream = CFWriteStreamCreateWithFile(kCFAllocatorDefault, m_metaDataUrl);
if (writeStream) {
if (CFWriteStreamOpen(writeStream)) {
CFStringRef contentType = m_target->contentType();
UInt8 buf[1024];
CFIndex usedBytes = 0;
if (contentType) {
// It is possible that some streams don't provide a content type
CFStringGetBytes(contentType,
CFRangeMake(0, CFStringGetLength(contentType)),
kCFStringEncodingUTF8,
'?',
false,
buf,
1024,
&usedBytes);
}
if (usedBytes > 0) {
CS_TRACE("Writing the meta data\n");
CS_TRACE_CFSTRING(contentType);
CFWriteStreamWrite(writeStream, buf, usedBytes);
}
CFWriteStreamClose(writeStream);
}
CFRelease(writeStream);
}
m_cacheable = false;
m_writable = false;
m_useCache = true;
m_cacheMetaDataWritten = true;
}
}
}
if (m_delegate) {
m_delegate->streamEndEncountered();
}
}
void Caching_Stream::streamErrorOccurred(CFStringRef errorDesc)
{
if (m_delegate) {
m_delegate->streamErrorOccurred(errorDesc);
}
}
void Caching_Stream::streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
{
if (m_delegate) {
m_delegate->streamMetaDataAvailable(metaData);
}
}
void Caching_Stream::streamMetaDataByteSizeAvailable(UInt32 sizeInBytes)
{
if (m_delegate) {
m_delegate->streamMetaDataByteSizeAvailable(sizeInBytes);
}
}
} // namespace astreamer

View File

@ -0,0 +1,73 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#ifndef ASTREAMER_CACHING_STREAM_H
#define ASTREAMER_CACHING_STREAM_H
#include "input_stream.h"
namespace astreamer {
class File_Output;
class File_Stream;
class Caching_Stream : public Input_Stream, public Input_Stream_Delegate {
private:
Input_Stream *m_target;
File_Output *m_fileOutput;
File_Stream *m_fileStream;
bool m_cacheable;
bool m_writable;
bool m_useCache;
bool m_cacheMetaDataWritten;
CFStringRef m_cacheIdentifier;
CFURLRef m_fileUrl;
CFURLRef m_metaDataUrl;
private:
CFURLRef createFileURLWithPath(CFStringRef path);
void readMetaData();
public:
Caching_Stream(Input_Stream *target);
virtual ~Caching_Stream();
Input_Stream_Position position();
CFStringRef contentType();
size_t contentLength();
bool open();
bool open(const Input_Stream_Position& position);
void close();
void setScheduledInRunLoop(bool scheduledInRunLoop);
void setUrl(CFURLRef url);
void setCacheIdentifier(CFStringRef cacheIdentifier);
static bool canHandleUrl(CFURLRef url);
/* ID3_Parser_Delegate */
void id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
void id3tagSizeAvailable(UInt32 tagSize);
void streamIsReadyRead();
void streamHasBytesAvailable(UInt8 *data, UInt32 numBytes);
void streamEndEncountered();
void streamErrorOccurred(CFStringRef errorDesc);
void streamMetaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
void streamMetaDataByteSizeAvailable(UInt32 sizeInBytes);
};
} // namespace astreamer
#endif /* ASTREAMER_CACHING_STREAM_H */

View File

@ -0,0 +1,30 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#include "file_output.h"
namespace astreamer {
File_Output::File_Output(CFURLRef fileURL) :
m_writeStream(CFWriteStreamCreateWithFile(kCFAllocatorDefault, fileURL))
{
CFWriteStreamOpen(m_writeStream);
}
File_Output::~File_Output()
{
CFWriteStreamClose(m_writeStream);
CFRelease(m_writeStream);
}
CFIndex File_Output::write(const UInt8 *buffer, CFIndex bufferLength)
{
return CFWriteStreamWrite(m_writeStream, buffer, bufferLength);
}
} // namespace astreamer

View File

@ -0,0 +1,32 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#ifndef ASTREAMER_FILE_OUTPUT_H
#define ASTREAMER_FILE_OUTPUT_H
#import <CoreFoundation/CoreFoundation.h>
namespace astreamer {
class File_Output {
private:
File_Output(const File_Output&);
File_Output& operator=(const File_Output&);
CFWriteStreamRef m_writeStream;
public:
File_Output(CFURLRef fileURL);
~File_Output();
CFIndex write(const UInt8 *buffer, CFIndex bufferLength);
};
} // namespace astreamer
#endif // ASTREAMER_FILE_OUTPUT_H

View File

@ -0,0 +1,405 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#include "file_stream.h"
#include "stream_configuration.h"
namespace astreamer {
File_Stream::File_Stream() :
m_url(0),
m_readStream(0),
m_scheduledInRunLoop(false),
m_readPending(false),
m_fileReadBuffer(0),
m_id3Parser(new ID3_Parser()),
m_contentType(0)
{
m_id3Parser->m_delegate = this;
}
File_Stream::~File_Stream()
{
close();
if (m_fileReadBuffer) {
delete [] m_fileReadBuffer;
m_fileReadBuffer = 0;
}
if (m_url) {
CFRelease(m_url);
m_url = 0;
}
delete m_id3Parser;
m_id3Parser = 0;
if (m_contentType) {
CFRelease(m_contentType);
}
}
Input_Stream_Position File_Stream::position()
{
return m_position;
}
CFStringRef File_Stream::contentType()
{
if (m_contentType) {
// Use the provided content type
return m_contentType;
}
// Try to resolve the content type from the file
CFStringRef contentType = CFSTR("");
CFStringRef pathComponent = 0;
CFIndex len = 0;
CFRange range;
CFStringRef suffix = 0;
if (!m_url) {
goto done;
}
pathComponent = CFURLCopyLastPathComponent(m_url);
if (!pathComponent) {
goto done;
}
len = CFStringGetLength(pathComponent);
if (len > 5) {
range.length = 4;
range.location = len - 4;
suffix = CFStringCreateWithSubstring(kCFAllocatorDefault,
pathComponent,
range);
if (!suffix) {
goto done;
}
// TODO: we should do the content-type resolvation in a better way.
if (CFStringCompare(suffix, CFSTR(".mp3"), 0) == kCFCompareEqualTo) {
contentType = CFSTR("audio/mpeg");
} else if (CFStringCompare(suffix, CFSTR(".m4a"), 0) == kCFCompareEqualTo) {
contentType = CFSTR("audio/x-m4a");
} else if (CFStringCompare(suffix, CFSTR(".mp4"), 0) == kCFCompareEqualTo) {
contentType = CFSTR("audio/mp4");
} else if (CFStringCompare(suffix, CFSTR(".aac"), 0) == kCFCompareEqualTo) {
contentType = CFSTR("audio/aac");
}
}
done:
if (pathComponent) {
CFRelease(pathComponent);
}
if (suffix) {
CFRelease(suffix);
}
return contentType;
}
void File_Stream::setContentType(CFStringRef contentType)
{
if (m_contentType) {
CFRelease(m_contentType);
m_contentType = 0;
}
if (contentType) {
m_contentType = CFStringCreateCopy(kCFAllocatorDefault, contentType);
}
}
size_t File_Stream::contentLength()
{
CFNumberRef length = NULL;
CFErrorRef err = NULL;
if (CFURLCopyResourcePropertyForKey(m_url, kCFURLFileSizeKey, &length, &err)) {
CFIndex fileLength;
if (CFNumberGetValue(length, kCFNumberCFIndexType, &fileLength)) {
CFRelease(length);
return fileLength;
}
}
return 0;
}
bool File_Stream::open()
{
Input_Stream_Position position;
position.start = 0;
position.end = 0;
m_id3Parser->reset();
return open(position);
}
bool File_Stream::open(const Input_Stream_Position& position)
{
bool success = false;
CFStreamClientContext CTX = { 0, this, NULL, NULL, NULL };
/* Already opened a read stream, return */
if (m_readStream) {
goto out;
}
if (!m_url) {
goto out;
}
/* Reset state */
m_position = position;
m_readPending = false;
/* Failed to create a stream */
if (!(m_readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, m_url))) {
goto out;
}
if (m_position.start > 0) {
CFNumberRef position = CFNumberCreate(0, kCFNumberLongLongType, &m_position.start);
CFReadStreamSetProperty(m_readStream, kCFStreamPropertyFileCurrentOffset, position);
CFRelease(position);
}
if (!CFReadStreamSetClient(m_readStream, kCFStreamEventHasBytesAvailable |
kCFStreamEventEndEncountered |
kCFStreamEventErrorOccurred, readCallBack, &CTX)) {
CFRelease(m_readStream);
m_readStream = 0;
goto out;
}
setScheduledInRunLoop(true);
if (!CFReadStreamOpen(m_readStream)) {
/* Open failed: clean */
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
setScheduledInRunLoop(false);
if (m_readStream) {
CFRelease(m_readStream);
m_readStream = 0;
}
goto out;
}
success = true;
out:
if (success) {
if (m_delegate) {
m_delegate->streamIsReadyRead();
}
}
return success;
}
void File_Stream::close()
{
/* The stream has been already closed */
if (!m_readStream) {
return;
}
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
setScheduledInRunLoop(false);
CFReadStreamClose(m_readStream);
CFRelease(m_readStream);
m_readStream = 0;
}
void File_Stream::setScheduledInRunLoop(bool scheduledInRunLoop)
{
/* The stream has not been opened, or it has been already closed */
if (!m_readStream) {
return;
}
/* The state doesn't change */
if (m_scheduledInRunLoop == scheduledInRunLoop) {
return;
}
if (m_scheduledInRunLoop) {
CFReadStreamUnscheduleFromRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
} else {
if (m_readPending) {
m_readPending = false;
readCallBack(m_readStream, kCFStreamEventHasBytesAvailable, this);
}
CFReadStreamScheduleWithRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
}
m_scheduledInRunLoop = scheduledInRunLoop;
}
void File_Stream::setUrl(CFURLRef url)
{
if (m_url) {
CFRelease(m_url);
}
if (url) {
m_url = (CFURLRef)CFRetain(url);
} else {
m_url = NULL;
}
}
bool File_Stream::canHandleUrl(CFURLRef url)
{
if (!url) {
return false;
}
CFStringRef scheme = CFURLCopyScheme(url);
if (scheme) {
if (CFStringCompare(scheme, CFSTR("file"), 0) == kCFCompareEqualTo) {
CFRelease(scheme);
// The only scheme we claim to handle are the local files
return true;
}
CFRelease(scheme);
}
// We don't handle anything else but local files
return false;
}
/* ID3_Parser_Delegate */
void File_Stream::id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
{
if (m_delegate) {
m_delegate->streamMetaDataAvailable(metaData);
}
}
void File_Stream::id3tagSizeAvailable(UInt32 tagSize)
{
if (m_delegate) {
m_delegate->streamMetaDataByteSizeAvailable(tagSize);
}
}
void File_Stream::readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo)
{
File_Stream *THIS = static_cast<File_Stream*>(clientCallBackInfo);
Stream_Configuration *config = Stream_Configuration::configuration();
switch (eventType) {
case kCFStreamEventHasBytesAvailable: {
if (!THIS->m_fileReadBuffer) {
THIS->m_fileReadBuffer = new UInt8[config->httpConnectionBufferSize];
}
while (CFReadStreamHasBytesAvailable(stream)) {
if (!THIS->m_scheduledInRunLoop) {
/*
* This is critical - though the stream has data available,
* do not try to feed the audio queue with data, if it has
* indicated that it doesn't want more data due to buffers
* full.
*/
THIS->m_readPending = true;
break;
}
CFIndex bytesRead = CFReadStreamRead(stream, THIS->m_fileReadBuffer, config->httpConnectionBufferSize);
if (CFReadStreamGetStatus(stream) == kCFStreamStatusError ||
bytesRead < 0) {
if (THIS->m_delegate) {
CFStringRef reportedNetworkError = NULL;
CFErrorRef streamError = CFReadStreamCopyError(stream);
if (streamError) {
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
if (errorDesc) {
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
CFRelease(errorDesc);
}
CFRelease(streamError);
}
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
if (reportedNetworkError) {
CFRelease(reportedNetworkError);
}
}
break;
}
if (bytesRead > 0) {
if (THIS->m_delegate) {
THIS->m_delegate->streamHasBytesAvailable(THIS->m_fileReadBuffer, (UInt32)bytesRead);
}
if (THIS->m_id3Parser->wantData()) {
THIS->m_id3Parser->feedData(THIS->m_fileReadBuffer, (UInt32)bytesRead);
}
}
}
break;
}
case kCFStreamEventEndEncountered: {
if (THIS->m_delegate) {
THIS->m_delegate->streamEndEncountered();
}
break;
}
case kCFStreamEventErrorOccurred: {
if (THIS->m_delegate) {
CFStringRef reportedNetworkError = NULL;
CFErrorRef streamError = CFReadStreamCopyError(stream);
if (streamError) {
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
if (errorDesc) {
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
CFRelease(errorDesc);
}
CFRelease(streamError);
}
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
if (reportedNetworkError) {
CFRelease(reportedNetworkError);
}
}
break;
}
}
}
} // namespace astreamer

View File

@ -0,0 +1,64 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#ifndef ASTREAMER_FILE_STREAM_H
#define ASTREAMER_FILE_STREAM_H
#import "input_stream.h"
#import "id3_parser.h"
namespace astreamer {
class File_Stream : public Input_Stream {
private:
File_Stream(const File_Stream&);
File_Stream& operator=(const File_Stream&);
CFURLRef m_url;
CFReadStreamRef m_readStream;
bool m_scheduledInRunLoop;
bool m_readPending;
Input_Stream_Position m_position;
UInt8 *m_fileReadBuffer;
ID3_Parser *m_id3Parser;
CFStringRef m_contentType;
static void readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo);
public:
File_Stream();
virtual ~File_Stream();
Input_Stream_Position position();
CFStringRef contentType();
void setContentType(CFStringRef contentType);
size_t contentLength();
bool open();
bool open(const Input_Stream_Position& position);
void close();
void setScheduledInRunLoop(bool scheduledInRunLoop);
void setUrl(CFURLRef url);
static bool canHandleUrl(CFURLRef url);
/* ID3_Parser_Delegate */
void id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData);
void id3tagSizeAvailable(UInt32 tagSize);
};
} // namespace astreamer
#endif // ASTREAMER_FILE_STREAM_H

View File

@ -0,0 +1,908 @@
/*
* This file is part of the FreeStreamer project,
* (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi>
* See the file ''LICENSE'' for using the code.
*
* https://github.com/muhku/FreeStreamer
*/
#include "http_stream.h"
#include "audio_queue.h"
#include "id3_parser.h"
#include "stream_configuration.h"
//#define HS_DEBUG 1
#if !defined (HS_DEBUG)
#define HS_TRACE(...) do {} while (0)
#define HS_TRACE_CFSTRING(X) do {} while (0)
#else
#define HS_TRACE(...) printf(__VA_ARGS__)
#define HS_TRACE_CFSTRING(X) HS_TRACE("%s\n", CFStringGetCStringPtr(X, kCFStringEncodingMacRoman))
#endif
/*
* Comment the following line to disable ID3 tag support:
*/
#define INCLUDE_ID3TAG_SUPPORT 1
namespace astreamer {
CFStringRef HTTP_Stream::httpRequestMethod = CFSTR("GET");
CFStringRef HTTP_Stream::httpUserAgentHeader = CFSTR("User-Agent");
CFStringRef HTTP_Stream::httpRangeHeader = CFSTR("Range");
CFStringRef HTTP_Stream::icyMetaDataHeader = CFSTR("Icy-MetaData");
CFStringRef HTTP_Stream::icyMetaDataValue = CFSTR("1"); /* always request ICY metadata, if available */
/* HTTP_Stream: public */
HTTP_Stream::HTTP_Stream() :
m_readStream(0),
m_scheduledInRunLoop(false),
m_readPending(false),
m_url(0),
m_httpHeadersParsed(false),
m_contentType(0),
m_contentLength(0),
m_bytesRead(0),
m_icyStream(false),
m_icyHeaderCR(false),
m_icyHeadersRead(false),
m_icyHeadersParsed(false),
m_icyName(0),
m_icyMetaDataInterval(0),
m_dataByteReadCount(0),
m_metaDataBytesRemaining(0),
m_httpReadBuffer(0),
m_icyReadBuffer(0),
m_id3Parser(new ID3_Parser())
{
m_id3Parser->m_delegate = this;
}
HTTP_Stream::~HTTP_Stream()
{
close();
for (std::vector<CFStringRef>::iterator h = m_icyHeaderLines.begin(); h != m_icyHeaderLines.end(); ++h) {
CFRelease(*h);
}
m_icyHeaderLines.clear();
if (m_contentType) {
CFRelease(m_contentType);
m_contentType = 0;
}
if (m_icyName) {
CFRelease(m_icyName);
m_icyName = 0;
}
if (m_httpReadBuffer) {
delete [] m_httpReadBuffer;
m_httpReadBuffer = 0;
}
if (m_icyReadBuffer) {
delete [] m_icyReadBuffer;
m_icyReadBuffer = 0;
}
if (m_url) {
CFRelease(m_url);
m_url = 0;
}
delete m_id3Parser;
m_id3Parser = 0;
}
Input_Stream_Position HTTP_Stream::position()
{
return m_position;
}
CFStringRef HTTP_Stream::contentType()
{
return m_contentType;
}
size_t HTTP_Stream::contentLength()
{
return m_contentLength;
}
bool HTTP_Stream::open()
{
Input_Stream_Position position;
position.start = 0;
position.end = 0;
m_contentLength = 0;
#ifdef INCLUDE_ID3TAG_SUPPORT
m_id3Parser->reset();
#endif
return open(position);
}
bool HTTP_Stream::open(const Input_Stream_Position& position)
{
bool success = false;
CFStreamClientContext CTX = { 0, this, NULL, NULL, NULL };
/* Already opened a read stream, return */
if (m_readStream) {
goto out;
}
/* Reset state */
m_position = position;
m_readPending = false;
m_httpHeadersParsed = false;
if (m_contentType) {
CFRelease(m_contentType);
m_contentType = NULL;
}
m_icyStream = false;
m_icyHeaderCR = false;
m_icyHeadersRead = false;
m_icyHeadersParsed = false;
if (m_icyName) {
CFRelease(m_icyName);
m_icyName = 0;
}
for (std::vector<CFStringRef>::iterator h = m_icyHeaderLines.begin(); h != m_icyHeaderLines.end(); ++h) {
CFRelease(*h);
}
m_icyHeaderLines.clear();
m_icyMetaDataInterval = 0;
m_dataByteReadCount = 0;
m_metaDataBytesRemaining = 0;
m_bytesRead = 0;
if (!m_url) {
goto out;
}
/* Failed to create a stream */
if (!(m_readStream = createReadStream(m_url))) {
goto out;
}
if (!CFReadStreamSetClient(m_readStream, kCFStreamEventHasBytesAvailable |
kCFStreamEventEndEncountered |
kCFStreamEventErrorOccurred, readCallBack, &CTX)) {
CFRelease(m_readStream);
m_readStream = 0;
goto out;
}
setScheduledInRunLoop(true);
if (!CFReadStreamOpen(m_readStream)) {
/* Open failed: clean */
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
setScheduledInRunLoop(false);
if (m_readStream) {
CFRelease(m_readStream);
m_readStream = 0;
}
goto out;
}
success = true;
out:
return success;
}
void HTTP_Stream::close()
{
/* The stream has been already closed */
if (!m_readStream) {
return;
}
CFReadStreamSetClient(m_readStream, 0, NULL, NULL);
setScheduledInRunLoop(false);
CFReadStreamClose(m_readStream);
CFRelease(m_readStream);
m_readStream = 0;
}
void HTTP_Stream::setScheduledInRunLoop(bool scheduledInRunLoop)
{
/* The stream has not been opened, or it has been already closed */
if (!m_readStream) {
return;
}
/* The state doesn't change */
if (m_scheduledInRunLoop == scheduledInRunLoop) {
return;
}
if (m_scheduledInRunLoop) {
CFReadStreamUnscheduleFromRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
} else {
if (m_readPending) {
m_readPending = false;
readCallBack(m_readStream, kCFStreamEventHasBytesAvailable, this);
}
CFReadStreamScheduleWithRunLoop(m_readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
}
m_scheduledInRunLoop = scheduledInRunLoop;
}
void HTTP_Stream::setUrl(CFURLRef url)
{
if (m_url) {
CFRelease(m_url);
}
if (url) {
m_url = (CFURLRef)CFRetain(url);
} else {
m_url = NULL;
}
}
bool HTTP_Stream::canHandleUrl(CFURLRef url)
{
if (!url) {
return false;
}
CFStringRef scheme = CFURLCopyScheme(url);
if (scheme) {
if (CFStringCompare(scheme, CFSTR("file"), 0) == kCFCompareEqualTo) {
CFRelease(scheme);
// The only scheme we claim not to handle are local files.
return false;
}
CFRelease(scheme);
}
return true;
}
void HTTP_Stream::id3metaDataAvailable(std::map<CFStringRef,CFStringRef> metaData)
{
if (m_delegate) {
m_delegate->streamMetaDataAvailable(metaData);
}
}
void HTTP_Stream::id3tagSizeAvailable(UInt32 tagSize)
{
if (m_delegate) {
m_delegate->streamMetaDataByteSizeAvailable(tagSize);
}
}
/* private */
CFReadStreamRef HTTP_Stream::createReadStream(CFURLRef url)
{
CFReadStreamRef readStream = 0;
CFHTTPMessageRef request = 0;
CFDictionaryRef proxySettings = 0;
Stream_Configuration *config = Stream_Configuration::configuration();
if (!(request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, httpRequestMethod, url, kCFHTTPVersion1_1))) {
goto out;
}
if (config->userAgent) {
CFHTTPMessageSetHeaderFieldValue(request, httpUserAgentHeader, config->userAgent);
}
CFHTTPMessageSetHeaderFieldValue(request, icyMetaDataHeader, icyMetaDataValue);
if (m_position.start > 0 && m_position.end > m_position.start) {
CFStringRef rangeHeaderValue = CFStringCreateWithFormat(NULL,
NULL,
CFSTR("bytes=%llu-%llu"),
m_position.start,
m_position.end);
CFHTTPMessageSetHeaderFieldValue(request, httpRangeHeader, rangeHeaderValue);
CFRelease(rangeHeaderValue);
} else if (m_position.start > 0 && m_position.end < m_position.start) {
CFStringRef rangeHeaderValue = CFStringCreateWithFormat(NULL,
NULL,
CFSTR("bytes=%llu-"),
m_position.start);
CFHTTPMessageSetHeaderFieldValue(request, httpRangeHeader, rangeHeaderValue);
CFRelease(rangeHeaderValue);
}
if (config->predefinedHttpHeaderValues) {
const CFIndex numKeys = CFDictionaryGetCount(config->predefinedHttpHeaderValues);
if (numKeys > 0) {
CFTypeRef *keys = (CFTypeRef *) malloc(numKeys * sizeof(CFTypeRef));
if (keys) {
CFDictionaryGetKeysAndValues(config->predefinedHttpHeaderValues, (const void **) keys, NULL);
for (CFIndex i=0; i < numKeys; i++) {
CFTypeRef key = keys[i];
if (CFGetTypeID(key) == CFStringGetTypeID()) {
const void *value = CFDictionaryGetValue(config->predefinedHttpHeaderValues, (const void *) key);
if (value) {
CFStringRef headerKey = (CFStringRef) key;
CFTypeRef valueRef = (CFTypeRef) value;
if (CFGetTypeID(valueRef) == CFStringGetTypeID()) {
CFStringRef headerValue = (CFStringRef) valueRef;
HS_TRACE("Setting predefined HTTP header ");
HS_TRACE_CFSTRING(headerKey);
HS_TRACE_CFSTRING(headerValue);
CFHTTPMessageSetHeaderFieldValue(request, headerKey, headerValue);
}
}
}
}
free(keys);
}
}
}
if (!(readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request))) {
goto out;
}
CFReadStreamSetProperty(readStream,
kCFStreamNetworkServiceType,
kCFStreamNetworkServiceTypeBackground);
CFReadStreamSetProperty(readStream,
kCFStreamPropertyHTTPShouldAutoredirect,
kCFBooleanTrue);
proxySettings = CFNetworkCopySystemProxySettings();
if (proxySettings) {
CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, proxySettings);
CFRelease(proxySettings);
}
out:
if (request) {
CFRelease(request);
}
return readStream;
}
void HTTP_Stream::parseHttpHeadersIfNeeded(const UInt8 *buf, const CFIndex bufSize)
{
if (m_httpHeadersParsed) {
return;
}
m_httpHeadersParsed = true;
/* If the response has the "ICY 200 OK" string,
* we are dealing with the ShoutCast protocol.
* The HTTP headers won't be available.
*/
if (bufSize >= 10 &&
buf[0] == 0x49 && buf[1] == 0x43 && buf[2] == 0x59 &&
buf[3] == 0x20 && buf[4] == 0x32 && buf[5] == 0x30 &&
buf[6] == 0x30 && buf[7] == 0x20 && buf[8] == 0x4F &&
buf[9] == 0x4B) {
m_icyStream = true;
HS_TRACE("Detected an IceCast stream\n");
// This is an ICY stream, don't try to parse the HTTP headers
return;
}
HS_TRACE("A regular HTTP stream\n");
CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreamCopyProperty(m_readStream, kCFStreamPropertyHTTPResponseHeader);
CFIndex statusCode = 0;
if (response) {
/*
* If the server responded with the icy-metaint header, the response
* body will be encoded in the ShoutCast protocol.
*/
CFStringRef icyMetaIntString = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("icy-metaint"));
if (icyMetaIntString) {
m_icyStream = true;
m_icyHeadersParsed = true;
m_icyHeadersRead = true;
m_icyMetaDataInterval = CFStringGetIntValue(icyMetaIntString);
CFRelease(icyMetaIntString);
}
HS_TRACE("icy-metaint: %zu\n", m_icyMetaDataInterval);
statusCode = CFHTTPMessageGetResponseStatusCode(response);
HS_TRACE("HTTP response code %zu", statusCode);
CFStringRef icyNameString = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("icy-name"));
if (icyNameString) {
if (m_icyName) {
CFRelease(m_icyName);
}
m_icyName = icyNameString;
if (m_delegate) {
std::map<CFStringRef,CFStringRef> metadataMap;
metadataMap[CFSTR("IcecastStationName")] = CFStringCreateCopy(kCFAllocatorDefault, m_icyName);
m_delegate->streamMetaDataAvailable(metadataMap);
}
}
if (m_contentType) {
CFRelease(m_contentType);
}
m_contentType = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("Content-Type"));
HS_TRACE("Content-type: ");
HS_TRACE_CFSTRING(m_contentType);
CFStringRef contentLengthString = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("Content-Length"));
if (contentLengthString) {
m_contentLength = CFStringGetIntValue(contentLengthString);
CFRelease(contentLengthString);
}
CFRelease(response);
}
if (m_delegate &&
(statusCode == 200 || statusCode == 206)) {
m_delegate->streamIsReadyRead();
} else {
if (m_delegate) {
CFStringRef statusCodeString = CFStringCreateWithFormat(NULL,
NULL,
CFSTR("HTTP response code %d"),
(unsigned int)statusCode);
m_delegate->streamErrorOccurred(statusCodeString);
if (statusCodeString) {
CFRelease(statusCodeString);
}
}
}
}
void HTTP_Stream::parseICYStream(const UInt8 *buf, const CFIndex bufSize)
{
HS_TRACE("Parsing an IceCast stream, received %li bytes\n", bufSize);
CFIndex offset = 0;
CFIndex bytesFound = 0;
if (!m_icyHeadersRead) {
HS_TRACE("ICY headers not read, reading\n");
for (; offset < bufSize; offset++) {
if (m_icyHeaderCR && buf[offset] == '\n') {
if (bytesFound > 0) {
m_icyHeaderLines.push_back(createMetaDataStringWithMostReasonableEncoding(&buf[offset-bytesFound-1], bytesFound));
bytesFound = 0;
HS_TRACE_CFSTRING(m_icyHeaderLines[m_icyHeaderLines.size()-1]);
continue;
}
HS_TRACE("End of ICY headers\n");
m_icyHeadersRead = true;
break;
}
if (buf[offset] == '\r') {
m_icyHeaderCR = true;
continue;
} else {
m_icyHeaderCR = false;
}
bytesFound++;
}
} else if (!m_icyHeadersParsed) {
HS_TRACE("ICY headers not parsed, parsing\n");
const CFStringRef icyContentTypeHeader = CFSTR("content-type:");
const CFStringRef icyMetaDataHeader = CFSTR("icy-metaint:");
const CFStringRef icyNameHeader = CFSTR("icy-name:");
const CFIndex icyContenTypeHeaderLength = CFStringGetLength(icyContentTypeHeader);
const CFIndex icyMetaDataHeaderLength = CFStringGetLength(icyMetaDataHeader);
const CFIndex icyNameHeaderLength = CFStringGetLength(icyNameHeader);
for (std::vector<CFStringRef>::iterator h = m_icyHeaderLines.begin(); h != m_icyHeaderLines.end(); ++h) {
CFStringRef line = *h;
const CFIndex lineLength = CFStringGetLength(line);
if (lineLength == 0) {
continue;
}
HS_TRACE_CFSTRING(line);
if (CFStringCompareWithOptions(line,
icyContentTypeHeader,
CFRangeMake(0, icyContenTypeHeaderLength),
0) == kCFCompareEqualTo) {
if (m_contentType) {
CFRelease(m_contentType);
m_contentType = 0;
}
m_contentType = CFStringCreateWithSubstring(kCFAllocatorDefault,
line,
CFRangeMake(icyContenTypeHeaderLength, lineLength - icyContenTypeHeaderLength));
}
if (CFStringCompareWithOptions(line,
icyMetaDataHeader,
CFRangeMake(0, icyMetaDataHeaderLength),
0) == kCFCompareEqualTo) {
CFStringRef metadataInterval = CFStringCreateWithSubstring(kCFAllocatorDefault,
line,
CFRangeMake(icyMetaDataHeaderLength, lineLength - icyMetaDataHeaderLength));
if (metadataInterval) {
m_icyMetaDataInterval = CFStringGetIntValue(metadataInterval);
CFRelease(metadataInterval);
} else {
m_icyMetaDataInterval = 0;
}
}
if (CFStringCompareWithOptions(line,
icyNameHeader,
CFRangeMake(0, icyNameHeaderLength),
0) == kCFCompareEqualTo) {
if (m_icyName) {
CFRelease(m_icyName);
}
m_icyName = CFStringCreateWithSubstring(kCFAllocatorDefault,
line,
CFRangeMake(icyNameHeaderLength, lineLength - icyNameHeaderLength));
}
}
m_icyHeadersParsed = true;
offset++;
if (m_delegate) {
m_delegate->streamIsReadyRead();
}
}
Stream_Configuration *config = Stream_Configuration::configuration();
if (!m_icyReadBuffer) {
m_icyReadBuffer = new UInt8[config->httpConnectionBufferSize];
}
HS_TRACE("Reading ICY stream for playback\n");
UInt32 i=0;
for (; offset < bufSize; offset++) {
// is this a metadata byte?
if (m_metaDataBytesRemaining > 0) {
m_metaDataBytesRemaining--;
if (m_metaDataBytesRemaining == 0) {
m_dataByteReadCount = 0;
if (m_delegate && !m_icyMetaData.empty()) {
std::map<CFStringRef,CFStringRef> metadataMap;
CFStringRef metaData = createMetaDataStringWithMostReasonableEncoding(&m_icyMetaData[0],
m_icyMetaData.size());
if (!metaData) {
// Metadata encoding failed, cannot parse.
m_icyMetaData.clear();
continue;
}
CFArrayRef tokens = CFStringCreateArrayBySeparatingStrings(kCFAllocatorDefault,
metaData,
CFSTR(";"));
for (CFIndex i=0, max=CFArrayGetCount(tokens); i < max; i++) {
CFStringRef token = (CFStringRef) CFArrayGetValueAtIndex(tokens, i);
CFRange foundRange;
if (CFStringFindWithOptions(token,
CFSTR("='"),
CFRangeMake(0, CFStringGetLength(token)),
NULL,
&foundRange) == true) {
CFRange keyRange = CFRangeMake(0, foundRange.location);
CFStringRef metadaKey = CFStringCreateWithSubstring(kCFAllocatorDefault,
token,
keyRange);
CFRange valueRange = CFRangeMake(foundRange.location + 2, CFStringGetLength(token) - keyRange.length - 3);
CFStringRef metadaValue = CFStringCreateWithSubstring(kCFAllocatorDefault,
token,
valueRange);
metadataMap[metadaKey] = metadaValue;
}
}
CFRelease(tokens);
CFRelease(metaData);
if (m_icyName) {
metadataMap[CFSTR("IcecastStationName")] = CFStringCreateCopy(kCFAllocatorDefault, m_icyName);
}
m_delegate->streamMetaDataAvailable(metadataMap);
}
m_icyMetaData.clear();
continue;
}
m_icyMetaData.push_back(buf[offset]);
continue;
}
// is this the interval byte?
if (m_icyMetaDataInterval > 0 && m_dataByteReadCount == m_icyMetaDataInterval) {
m_metaDataBytesRemaining = buf[offset] * 16;
if (m_metaDataBytesRemaining == 0) {
m_dataByteReadCount = 0;
}
continue;
}
// a data byte
m_dataByteReadCount++;
m_icyReadBuffer[i++] = buf[offset];
}
if (m_delegate && i > 0) {
m_delegate->streamHasBytesAvailable(m_icyReadBuffer, i);
}
}
#define TRY_ENCODING(STR,ENC) STR = CFStringCreateWithBytes(kCFAllocatorDefault, bytes, numBytes, ENC, false); \
if (STR != NULL) { return STR; }
CFStringRef HTTP_Stream::createMetaDataStringWithMostReasonableEncoding(const UInt8 *bytes, const CFIndex numBytes)
{
CFStringRef metaData;
TRY_ENCODING(metaData, kCFStringEncodingUTF8);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin1);
TRY_ENCODING(metaData, kCFStringEncodingWindowsLatin1);
TRY_ENCODING(metaData, kCFStringEncodingNextStepLatin);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin2);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin3);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin4);
TRY_ENCODING(metaData, kCFStringEncodingISOLatinCyrillic);
TRY_ENCODING(metaData, kCFStringEncodingISOLatinArabic);
TRY_ENCODING(metaData, kCFStringEncodingISOLatinGreek);
TRY_ENCODING(metaData, kCFStringEncodingISOLatinHebrew);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin5);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin6);
TRY_ENCODING(metaData, kCFStringEncodingISOLatinThai);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin7);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin8);
TRY_ENCODING(metaData, kCFStringEncodingISOLatin9);
TRY_ENCODING(metaData, kCFStringEncodingWindowsLatin2);
TRY_ENCODING(metaData, kCFStringEncodingWindowsCyrillic);
TRY_ENCODING(metaData, kCFStringEncodingWindowsGreek);
TRY_ENCODING(metaData, kCFStringEncodingWindowsLatin5);
TRY_ENCODING(metaData, kCFStringEncodingWindowsHebrew);
TRY_ENCODING(metaData, kCFStringEncodingWindowsArabic);
TRY_ENCODING(metaData, kCFStringEncodingKOI8_R);
TRY_ENCODING(metaData, kCFStringEncodingBig5);
TRY_ENCODING(metaData, kCFStringEncodingASCII);
return metaData;
}
#undef TRY_ENCODING
void HTTP_Stream::readCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo)
{
HTTP_Stream *THIS = static_cast<HTTP_Stream*>(clientCallBackInfo);
Stream_Configuration *config = Stream_Configuration::configuration();
CFStringRef reportedNetworkError = NULL;
switch (eventType) {
case kCFStreamEventHasBytesAvailable: {
if (!THIS->m_httpReadBuffer) {
THIS->m_httpReadBuffer = new UInt8[config->httpConnectionBufferSize];
}
while (CFReadStreamHasBytesAvailable(stream)) {
if (!THIS->m_scheduledInRunLoop) {
/*
* This is critical - though the stream has data available,
* do not try to feed the audio queue with data, if it has
* indicated that it doesn't want more data due to buffers
* full.
*/
THIS->m_readPending = true;
break;
}
CFIndex bytesRead = CFReadStreamRead(stream, THIS->m_httpReadBuffer, config->httpConnectionBufferSize);
if (CFReadStreamGetStatus(stream) == kCFStreamStatusError ||
bytesRead < 0) {
if (THIS->contentLength() > 0) {
/*
* Try to recover gracefully if we have a non-continuous stream
*/
Input_Stream_Position currentPosition = THIS->position();
Input_Stream_Position recoveryPosition;
recoveryPosition.start = currentPosition.start + THIS->m_bytesRead;
recoveryPosition.end = THIS->contentLength();
HS_TRACE("Recovering HTTP stream, start %llu\n", recoveryPosition.start);
THIS->open(recoveryPosition);
break;
}
CFErrorRef streamError = CFReadStreamCopyError(stream);
if (streamError) {
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
if (errorDesc) {
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
CFRelease(errorDesc);
}
CFRelease(streamError);
}
if (THIS->m_delegate) {
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
if (reportedNetworkError) {
CFRelease(reportedNetworkError);
reportedNetworkError = NULL;
}
}
break;
}
if (bytesRead > 0) {
THIS->m_bytesRead += bytesRead;
HS_TRACE("Read %li bytes, total %llu\n", bytesRead, THIS->m_bytesRead);
THIS->parseHttpHeadersIfNeeded(THIS->m_httpReadBuffer, bytesRead);
#ifdef INCLUDE_ID3TAG_SUPPORT
if (!THIS->m_icyStream && THIS->m_id3Parser->wantData()) {
THIS->m_id3Parser->feedData(THIS->m_httpReadBuffer, (UInt32)bytesRead);
}
#endif
if (THIS->m_icyStream) {
HS_TRACE("Parsing ICY stream\n");
THIS->parseICYStream(THIS->m_httpReadBuffer, bytesRead);
} else {
if (THIS->m_delegate) {
HS_TRACE("Not an ICY stream; calling the delegate back\n");
THIS->m_delegate->streamHasBytesAvailable(THIS->m_httpReadBuffer, (UInt32)bytesRead);
}
}
}
}
if (reportedNetworkError) {
CFRelease(reportedNetworkError);
reportedNetworkError = NULL;
}
break;
}
case kCFStreamEventEndEncountered: {
// This should concerns only non-continous streams
if (THIS->m_bytesRead < THIS->contentLength()) {
HS_TRACE("End of stream, but we have read only %llu bytes on a total of %li. Missing: %llu\n", THIS->m_bytesRead, THIS->contentLength(), (THIS->contentLength() - THIS->m_bytesRead));
Input_Stream_Position currentPosition = THIS->position();
Input_Stream_Position recoveryPosition;
recoveryPosition.start = currentPosition.start + THIS->m_bytesRead;
recoveryPosition.end = THIS->contentLength();
HS_TRACE("Reopen for the end of the file from byte position: %llu\n", recoveryPosition.start);
THIS->close();
THIS->open(recoveryPosition);
break;
}
if (THIS->m_delegate) {
THIS->m_delegate->streamEndEncountered();
}
break;
}
case kCFStreamEventErrorOccurred: {
if (THIS->m_delegate) {
CFStringRef reportedNetworkError = NULL;
CFErrorRef streamError = CFReadStreamCopyError(stream);
if (streamError) {
CFStringRef errorDesc = CFErrorCopyDescription(streamError);
if (errorDesc) {
reportedNetworkError = CFStringCreateCopy(kCFAllocatorDefault, errorDesc);
CFRelease(errorDesc);
}
CFRelease(streamError);
}
THIS->m_delegate->streamErrorOccurred(reportedNetworkError);
if (reportedNetworkError) {
CFRelease(reportedNetworkError);
}
}
break;
}
}
}
} // namespace astreamer

Some files were not shown because too many files have changed in this diff Show More