b面初次测试版
@ -32,6 +32,16 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@ -44,6 +54,8 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBFA9192BBA846600057FD5 /* CoreDataDelegete.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 */; };
|
||||
CBC54E652BC4D5D3003B1901 /* Summer Insects.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = CBC54E552BC4D5D3003B1901 /* Summer Insects.mp3 */; };
|
||||
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 */; };
|
||||
CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCAFB592BB3C2A000BC6520 /* LayoutConstraint.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 */; };
|
||||
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 */; };
|
||||
CBCF94D42BFED7AD0069EE0B /* AVPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBCF94D32BFED7AD0069EE0B /* AVPlayerItem.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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@ -222,6 +236,16 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -234,6 +258,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -256,7 +282,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -324,6 +349,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -348,6 +374,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -729,11 +757,12 @@
|
||||
CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */,
|
||||
CBD6F2152BF48DDD00343A4A /* MPPositive_ArtistHeaderModel.swift */,
|
||||
CBB5D31E2BDF711600CC333D /* MPPositive_SongItemModel.swift */,
|
||||
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */,
|
||||
CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */,
|
||||
CBB5F1F82BFC35D000CBF73A /* MPPositive_CollectionSongModel.swift */,
|
||||
CBB5F1FA2BFC3DB600CBF73A /* MPPositive_CollectionListModel.swift */,
|
||||
CBB5F1FC2BFC40E400CBF73A /* MPPositive_CollectionArtistModel.swift */,
|
||||
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */,
|
||||
CBE10CB42C0629B50068A396 /* MPPositive_SearchTagModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@ -752,6 +781,10 @@
|
||||
CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */,
|
||||
CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */,
|
||||
CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */,
|
||||
CB2416902C05D36F007877F7 /* MPPositive_DownloadViewModel.swift */,
|
||||
CB2416922C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift */,
|
||||
CB2416942C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift */,
|
||||
CB2416962C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift */,
|
||||
);
|
||||
path = ListViewModels;
|
||||
sourceTree = "<group>";
|
||||
@ -763,6 +796,7 @@
|
||||
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */,
|
||||
CBF456E22BF2086600ABF761 /* MPPositive_SearchResultsLoadViewModel.swift */,
|
||||
CB102F592BFB002C00E967D8 /* MPPositive_RecommendLoadViewModel.swift */,
|
||||
CB24168E2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift */,
|
||||
);
|
||||
path = LoadViewModels;
|
||||
sourceTree = "<group>";
|
||||
@ -846,6 +880,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CB0918A22BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift */,
|
||||
CB2416982C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift */,
|
||||
CB24169C2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift */,
|
||||
CB24169E2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift */,
|
||||
);
|
||||
path = "Center(个人曲库页)";
|
||||
sourceTree = "<group>";
|
||||
@ -862,6 +899,8 @@
|
||||
CBCB50202BD118BB009760B3 /* Center */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CB24168C2C05D09C007877F7 /* MPPositive_LibraryTableViewCell.swift */,
|
||||
CB24169A2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift */,
|
||||
);
|
||||
path = Center;
|
||||
sourceTree = "<group>";
|
||||
@ -869,6 +908,7 @@
|
||||
CBCB50212BD118BB009760B3 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CBE10CB62C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift */,
|
||||
CBF456EA2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift */,
|
||||
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */,
|
||||
CBF456EC2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift */,
|
||||
@ -963,11 +1003,12 @@
|
||||
CBE2C4CA2BC7BE5D00F283A7 /* MP_NetWorkManager.swift */,
|
||||
CB56612C2BE0DF8C00CFD014 /* MP_WebWork.swift */,
|
||||
CBB9F9DE2BEDDCC5008338DE /* MP_PlayerManager.swift */,
|
||||
CBBA6A212BFF12030047ADF8 /* MP_AVURLAsset.swift */,
|
||||
CBBA6A232BFF160C0047ADF8 /* MP_CacheManager.swift */,
|
||||
CBCB32192BD7578500802900 /* MP_LocationManager.swift */,
|
||||
CBBFA9172BBA83BA00057FD5 /* MP_CoreDataHandlerManager.swift */,
|
||||
CBE2C4C62BC783F700F283A7 /* MP_HUD.swift */,
|
||||
CBB5F1FE2BFCB40000CBF73A /* MPPositive_Debouncer.swift */,
|
||||
CBC687482BC2882B0023ECA6 /* MPTableManager.swift */,
|
||||
CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */,
|
||||
CB102F532BFAFA7200E967D8 /* MP_CircularProgressView.swift */,
|
||||
CB102F542BFAFA7200E967D8 /* MP_DownloadManager.swift */,
|
||||
@ -985,6 +1026,7 @@
|
||||
CBBFA91D2BBA9B5C00057FD5 /* Notification.swift */,
|
||||
CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */,
|
||||
CBE2C4C82BC7B25800F283A7 /* TableView.swift */,
|
||||
CBCF94D32BFED7AD0069EE0B /* AVPlayerItem.swift */,
|
||||
);
|
||||
path = "Extension(扩展)";
|
||||
sourceTree = "<group>";
|
||||
@ -1203,6 +1245,7 @@
|
||||
CBD6F21E2BF4B61F00343A4A /* MPPositive_ArtistShowTypeView.swift in Sources */,
|
||||
CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */,
|
||||
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */,
|
||||
CBE10CB52C0629B50068A396 /* MPPositive_SearchTagModel.swift in Sources */,
|
||||
CBF456E32BF2086600ABF761 /* MPPositive_SearchResultsLoadViewModel.swift in Sources */,
|
||||
CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */,
|
||||
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */,
|
||||
@ -1214,7 +1257,9 @@
|
||||
CBCB50162BD11402009760B3 /* MPSideA_Home_HeadBannerView.swift in Sources */,
|
||||
CBCC234F2BEE57AC004D7A57 /* MPPositive_PresentationController.swift in Sources */,
|
||||
CB102F5A2BFB002C00E967D8 /* MPPositive_RecommendLoadViewModel.swift in Sources */,
|
||||
CB24169B2C05E34D007877F7 /* MPPositive_LoveArtistTableViewCell.swift in Sources */,
|
||||
CBCB500A2BD11402009760B3 /* MPSideA_CustomTabBar.swift in Sources */,
|
||||
CB2416912C05D36F007877F7 /* MPPositive_DownloadViewModel.swift in Sources */,
|
||||
CBCAFB692BB3CAC400BC6520 /* MP_Lunch_ProgressView.swift in Sources */,
|
||||
CBD6F2182BF4A29B00343A4A /* MPPositive_ArtistViewModel.swift in Sources */,
|
||||
CBCB50102BD11402009760B3 /* MPSideA_SettingTableViewCell.swift in Sources */,
|
||||
@ -1232,11 +1277,13 @@
|
||||
CBCB4FFA2BD11402009760B3 /* MPSideA_PrivacyViewController.swift in Sources */,
|
||||
CBD6F21C2BF4AEE600343A4A /* MPPositive_ArtistShowHeaderView.swift in Sources */,
|
||||
CBCB500E2BD11402009760B3 /* MPSideA_CenterTableViewCell.swift in Sources */,
|
||||
CBBA6A222BFF12030047ADF8 /* MP_AVURLAsset.swift in Sources */,
|
||||
CB102F5E2BFB2F7C00E967D8 /* MPPositive_RecommendShowTypeView.swift in Sources */,
|
||||
009662312BB14A5A00FCA65F /* ViewController.swift in Sources */,
|
||||
CBEB01852BF5DB3400D45006 /* MPPositive_ArtistDescriptionTableViewCell.swift in Sources */,
|
||||
CBE2C4C72BC783F700F283A7 /* MP_HUD.swift in Sources */,
|
||||
CBE2C4C92BC7B25800F283A7 /* TableView.swift in Sources */,
|
||||
CB24168F2C05D2DD007877F7 /* MPPositive_LoadCoreModel.swift in Sources */,
|
||||
CBCB4F9A2BD11089009760B3 /* MP_NavigationController.swift in Sources */,
|
||||
CB0918A32BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift in Sources */,
|
||||
CBEE8E322BEB0FC0007DA798 /* MPPositive_PlayerCoverView.swift in Sources */,
|
||||
@ -1244,15 +1291,19 @@
|
||||
CBF456E92BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift in Sources */,
|
||||
CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */,
|
||||
CBE1CB4E2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift in Sources */,
|
||||
CB2416972C05D3C3007877F7 /* MPPositive_CollectionArtistViewModel.swift in Sources */,
|
||||
CBD313572BD63B390015D227 /* MPPositive_HomeListSecondCollectionViewCell.swift in Sources */,
|
||||
0096622D2BB14A5A00FCA65F /* AppDelegate.swift in Sources */,
|
||||
CBC32A532BD8D9F300687171 /* MPPositive_BrowseItemModel.swift in Sources */,
|
||||
CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */,
|
||||
CB24169F2C05EF1C007877F7 /* MPPositive_OfflineSongsViewController.swift in Sources */,
|
||||
CB102F552BFAFA7200E967D8 /* MP_CircularProgressView.swift in Sources */,
|
||||
CBCB4FEC2BD11402009760B3 /* MPSideA_AddViewController.swift in Sources */,
|
||||
CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */,
|
||||
CBCF94D42BFED7AD0069EE0B /* AVPlayerItem.swift in Sources */,
|
||||
CBCB50172BD11402009760B3 /* MPSideA_Home_RowListsTableViewCell.swift in Sources */,
|
||||
CBCB4F982BD11054009760B3 /* MP_BaseViewController.swift in Sources */,
|
||||
CB2416932C05D388007877F7 /* MPPositive_CollectionSongViewModel.swift in Sources */,
|
||||
CBD0CC5E2BDA260500C4B64D /* MPPositive_BrowseItemViewModel.swift in Sources */,
|
||||
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */,
|
||||
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */,
|
||||
@ -1264,10 +1315,12 @@
|
||||
CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */,
|
||||
CBCB4FE92BD11402009760B3 /* MPSideA_LoadDataMusic.swift in Sources */,
|
||||
CBCB4FEE2BD11402009760B3 /* MPSideA_BaseViewController.swift in Sources */,
|
||||
CB2416992C05DFC1007877F7 /* MPPositive_LoveArtistsViewController.swift in Sources */,
|
||||
CBE1CB582BDE550800701D57 /* MPPositive_ListShowViewController.swift in Sources */,
|
||||
CBCB500B2BD11402009760B3 /* MPSideA_CustomTabBarItem.swift in Sources */,
|
||||
CBB5D31D2BDF4E9600CC333D /* MPPositive_MusicItemShowTableViewCell.swift in Sources */,
|
||||
CBEB017D2BF5D35700D45006 /* MPPositive_ArtistShowSongTableViewCell.swift in Sources */,
|
||||
CB2416952C05D3A6007877F7 /* MPPositive_CollectionListViewModel.swift in Sources */,
|
||||
CBCC23512BEE58C1004D7A57 /* MPPositive_PlayerListShowViewController.swift in Sources */,
|
||||
CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */,
|
||||
CBB5F1F92BFC35D000CBF73A /* MPPositive_CollectionSongModel.swift in Sources */,
|
||||
@ -1284,10 +1337,11 @@
|
||||
CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */,
|
||||
CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.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 */,
|
||||
CBD313532BD60CD80015D227 /* MPPositive_HomeShowTableViewCell.swift in Sources */,
|
||||
CBD6F2162BF48DDD00343A4A /* MPPositive_ArtistHeaderModel.swift in Sources */,
|
||||
CB24169D2C05E89C007877F7 /* MPPositive_LoveSongsViewController.swift in Sources */,
|
||||
CB0918972BD25D8C006D2B39 /* MPPositive_TabBarController.swift in Sources */,
|
||||
CBCB500C2BD11402009760B3 /* MPSideA_CustomTabBarView.swift in Sources */,
|
||||
CBB9F9DD2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift in Sources */,
|
||||
@ -1303,9 +1357,11 @@
|
||||
CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */,
|
||||
CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */,
|
||||
CBCB4FF02BD11402009760B3 /* MPSideA_PresentationController.swift in Sources */,
|
||||
CBE10CB72C06373A0068A396 /* MPPositive_SearchTagCollectionViewCell.swift in Sources */,
|
||||
CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */,
|
||||
CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */,
|
||||
CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */,
|
||||
CBBA6A242BFF160C0047ADF8 /* MP_CacheManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -1459,7 +1515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1.2;
|
||||
DEVELOPMENT_TEAM = 6HWQW9JC74;
|
||||
DEVELOPMENT_TEAM = T93S37G27F;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MusicPlayer/Info.plist;
|
||||
@ -1497,7 +1553,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1.2;
|
||||
DEVELOPMENT_TEAM = 6HWQW9JC74;
|
||||
DEVELOPMENT_TEAM = T93S37G27F;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MusicPlayer/Info.plist;
|
||||
|
||||
@ -9,10 +9,13 @@ import UIKit
|
||||
import CoreData
|
||||
import AVFoundation
|
||||
import Alamofire
|
||||
import Tiercel
|
||||
@_exported import IQKeyboardManagerSwift
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
// 用来保存由系统提供的完成处理器
|
||||
var backgroundSessionCompletionHandler: (() -> Void)?
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
//请求通知权限
|
||||
UNUserNotificationCenter.current()
|
||||
@ -22,6 +25,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
print("Users are not allowed to be notified of messages.")
|
||||
}
|
||||
}
|
||||
//启动前销毁所有的下载任务
|
||||
DownloadManager.shared.cancelAllTasksIfNeeded()
|
||||
setAudioSupport()
|
||||
MP_NetWorkManager.shared.requestStatusToYouTube()
|
||||
IQKeyboardManager.shared.enable = true
|
||||
@ -32,15 +37,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return true
|
||||
}
|
||||
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
|
||||
DownloadManager.shared.session.getAllTasks { tasks in
|
||||
for task in tasks {
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
|
||||
if identifier == "com.yourApp.backgroundDownload" {
|
||||
DownloadManager.shared.session = SessionManager("com.yourApp.backgroundDownload", configuration: .init())
|
||||
DownloadManager.shared.session.completionHandler = completionHandler
|
||||
}
|
||||
}
|
||||
|
||||
//设置播放器会话状态
|
||||
private func setAudioSupport(){
|
||||
|
||||
22
MusicPlayer/Assets.xcassets/Positive/Center/Center_Add_'logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Center_Add_'logo.imageset/Group_1597880530@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Center_Add_'logo.imageset/Group_1597880530@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
22
MusicPlayer/Assets.xcassets/Positive/Center/Center_Top_bg.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Center_Top_bg.imageset/Gradient-09@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Center_Top_bg.imageset/Gradient-09@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 195 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
22
MusicPlayer/Assets.xcassets/Positive/Center/Love_Artists_logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Love_Artists_logo.imageset/Group_1597880746@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Love_Artists_logo.imageset/Group_1597880746@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
22
MusicPlayer/Assets.xcassets/Positive/Center/Love_Song_logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Love_Song_logo.imageset/Group_1597880536@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Love_Song_logo.imageset/Group_1597880536@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
22
MusicPlayer/Assets.xcassets/Positive/Center/Offline_Songs_logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Offline_Songs_logo.imageset/Group_1597880534@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
MusicPlayer/Assets.xcassets/Positive/Center/Offline_Songs_logo.imageset/Group_1597880534@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
22
MusicPlayer/Assets.xcassets/Positive/Search/Tag_Delete'logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Tag_Delete'logo.imageset/Frame@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Tag_Delete'logo.imageset/Frame@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
@ -20,5 +20,21 @@
|
||||
<string>"Musicoo" needs to obtain your location information in order to refine the preview music information provided to you!</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>"Musicoo" 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>
|
||||
</plist>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<?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">
|
||||
<attribute name="artistId" optional="YES" attributeType="String"/>
|
||||
<attribute name="coverImage" optional="YES" attributeType="URI"/>
|
||||
@ -10,26 +10,33 @@
|
||||
<attribute name="browseId" optional="YES" attributeType="String"/>
|
||||
<attribute name="coverImage" optional="YES" attributeType="URI"/>
|
||||
<attribute name="params" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="MPPositive_CollectionSongModel" representedClassName="MPPositive_CollectionSongModel" syncable="YES">
|
||||
<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="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="videoId" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<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="longBylineText" 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="resourcePath" optional="YES" attributeType="String"/>
|
||||
<attribute name="reviewImage" optional="YES" attributeType="URI"/>
|
||||
<attribute name="reviewImage" optional="YES" attributeType="String"/>
|
||||
<attribute name="shortBylineText" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="videoId" optional="YES" attributeType="String"/>
|
||||
</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">
|
||||
<attribute name="album" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
|
||||
22
MusicPlayer/MP/Common/Extension(扩展)/AVPlayerItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -79,6 +79,10 @@ extension NotificationCenter{
|
||||
case pup_player_vc
|
||||
///播放器页面更新
|
||||
case positive_player_reload
|
||||
///播放器预加载成功
|
||||
case positive_asset_successfully
|
||||
///用户手动调整了进度
|
||||
case positive_player_seek
|
||||
///用户切换播放器播放方式
|
||||
case player_type_switch
|
||||
///用户清空了歌单
|
||||
@ -87,6 +91,10 @@ extension NotificationCenter{
|
||||
case net_switch_notReachable
|
||||
///网络状态切换-网络可用
|
||||
case net_switch_reachable
|
||||
///b面pushj时
|
||||
case positive_nav_push
|
||||
///b面Pop时
|
||||
case positive_nav_pop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,24 @@ extension String {
|
||||
// 类type
|
||||
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 {
|
||||
func toNSRange(in string: String) -> NSRange {
|
||||
|
||||
@ -55,6 +55,22 @@ typealias ActionBlock = () -> Void?
|
||||
var MPSideA_ModalType:MPSideA_PresentModal = .Timer
|
||||
///B面全局模态弹出类型
|
||||
var MPPositive_ModalType:MPPositive_PresentModal = .PlayerList
|
||||
///沙盒文件
|
||||
let DocumentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
///获取沙盒下载路径
|
||||
func getDocumentsFileURL(_ videoID: String) -> String? {
|
||||
// 获取Documents目录的URL
|
||||
let documentsDirectoryURL = DocumentsURL.appendingPathComponent("Downloads")
|
||||
// 根据videoId构建文件完整URL
|
||||
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 {
|
||||
//设置分钟
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
//
|
||||
// MPTableManager.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/4/7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
///tableViewCell配置闭包(index-索引路径,tableView-当前表格,在VC中实现cell实体)
|
||||
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]]?
|
||||
///cell注册id组(与数据组组数相同)
|
||||
private var cellIdentifiers:[String]?
|
||||
//配置闭包
|
||||
private var configureCellBlock:TableViewCellConfigureBlock!
|
||||
//选中闭包
|
||||
private var didSelectBlock:TableViewCellDidSelectBlock?
|
||||
|
||||
/// 自定义UITableView控制器初始化方法
|
||||
/// - Parameters:
|
||||
/// - sectionItems: 包含组行的二维数组数据
|
||||
/// - cellIdentifier: cell注册ID组(与sectionItems中的组数相同)
|
||||
/// - 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
|
||||
}
|
||||
}
|
||||
//为tableView注册delegate,datasource,以及cell,自适应高度
|
||||
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)
|
||||
}
|
||||
}
|
||||
501
MusicPlayer/MP/Common/Tool(工具封装)/MP_AVURLAsset.swift
Normal 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)
|
||||
//对该Asset实现预加载,以让Asset触发resourceLoaderdelegate
|
||||
// 加载关键的播放属性
|
||||
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("用户滚动到了当前缓存范围外")
|
||||
// 用户seek位置在媒体数据范围内,假设这里已经有了初始URL
|
||||
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
|
||||
}
|
||||
143
MusicPlayer/MP/Common/Tool(工具封装)/MP_CacheManager.swift
Normal 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()
|
||||
// 缓存实体,字典形式,键值1为videoID,键值2为对应的资源数据
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,78 +7,85 @@
|
||||
|
||||
import Foundation
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
class DownloadManager: NSObject, URLSessionDownloadDelegate {
|
||||
import Tiercel
|
||||
class DownloadManager: NSObject {
|
||||
static let shared = DownloadManager()
|
||||
|
||||
var session: URLSession!
|
||||
var session: SessionManager!
|
||||
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 progressStorage: [URL: CGFloat] = [:] // 新增进度存储
|
||||
|
||||
var songHandlers:[URL: MPPositive_SongItemModel] = [:]
|
||||
private override init() {
|
||||
super.init()
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "com.yourApp.backgroundDownload")
|
||||
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
|
||||
var configuration = SessionConfiguration()
|
||||
configuration.timeoutIntervalForRequest = 60
|
||||
configuration.maxConcurrentTasksLimit = 6
|
||||
configuration.allowsCellularAccess = true
|
||||
session = SessionManager("com.yourApp.backgroundDownload", configuration: configuration)
|
||||
}
|
||||
|
||||
func downloadVideo(from url: URL, videoId: String, progressHandler: @escaping (CGFloat) -> Void, completion: @escaping (Result<URL, Error>) -> Void) {
|
||||
let downloadTask = session.downloadTask(with: url)
|
||||
func downloadVideo(from url: URL, song:MPPositive_SongItemModel, progressHandler: @escaping (CGFloat) -> Void, completion: @escaping (Result<MPPositive_SongItemModel, Error>) -> Void) {
|
||||
progressHandlers[url] = progressHandler
|
||||
completionHandlers[url] = completion
|
||||
downloadTasks[url] = downloadTask
|
||||
downloadTask.resume()
|
||||
}
|
||||
songHandlers[url] = song
|
||||
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? {
|
||||
return progressStorage[url]
|
||||
}
|
||||
|
||||
// URLSessionDownloadDelegate methods
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
guard let originalURL = downloadTask.originalRequest?.url else { return }
|
||||
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))
|
||||
func cancelAllTasksIfNeeded() {
|
||||
// 根据需求,取消所有任务,或者根据任务状态进行过滤
|
||||
for key in progressStorage.keys {
|
||||
session.cancel(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//class DownloadManager {
|
||||
//
|
||||
// static let shared = DownloadManager()
|
||||
|
||||
@ -64,6 +64,8 @@ class MP_NetWorkManager: NSObject {
|
||||
//MARK: - 固定参数
|
||||
//访问数据(首次首页预览时获得)
|
||||
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()
|
||||
//预览下一阶段参数(网络请求获取)
|
||||
@ -88,7 +90,7 @@ class MP_NetWorkManager: NSObject {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"hl":Language_first_local,
|
||||
@ -105,9 +107,11 @@ class MP_NetWorkManager: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
//MARK: - 异步队列
|
||||
//串行队列-预览
|
||||
//MARK: - GCD队列
|
||||
///串行队列-预览
|
||||
private var browseQueque:DispatchQueue?
|
||||
///并发队列-单曲资源预加载
|
||||
var playerItemLoadingGroup:DispatchGroup = DispatchGroup()
|
||||
//MARK: - 闭包
|
||||
///预览闭包(传递一个预览模块数据和完成状态)
|
||||
var browseRequestStateBlock:BrowseRequestStateBlock?
|
||||
@ -200,7 +204,7 @@ extension MP_NetWorkManager {
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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)
|
||||
}
|
||||
|
||||
@ -263,7 +267,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
comletion(results)
|
||||
}
|
||||
@ -320,7 +324,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
comletion(result)
|
||||
}
|
||||
@ -376,7 +380,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
comletion(result)
|
||||
}
|
||||
@ -429,7 +433,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
comletion(result)
|
||||
}
|
||||
@ -483,7 +487,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
//成功拿到列表所有歌曲(内容尚不完善)
|
||||
completion(listSongs)
|
||||
@ -534,7 +538,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(result)
|
||||
}
|
||||
@ -568,7 +572,7 @@ extension MP_NetWorkManager {
|
||||
//MARK: - 请求player播放资源
|
||||
/// 请求Player(单曲/视频)播放资源
|
||||
/// - 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路径
|
||||
let path = header+point+player
|
||||
//设置url
|
||||
@ -578,13 +582,27 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
//设置参数,videoId与params参数是必定携带内容
|
||||
let parameters:[String:Any] = [
|
||||
"videoId":(item.videoId ?? ""),
|
||||
"videoId":videoId,
|
||||
"prettyPrint":"false",
|
||||
// "playlistId":"OLAK5uy_knZiqQOlTDeQ3jecXrW_VIAZKdMnkLGgw",
|
||||
"context":[
|
||||
"client":[
|
||||
//当前访问版本(日期值)
|
||||
// //当前访问版本(日期值)
|
||||
// "clientName": "WEB_REMIX",
|
||||
// "clientVersion": "1.\(currTimeDate).01.00"
|
||||
//web端
|
||||
"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": [
|
||||
@ -593,13 +611,13 @@ extension MP_NetWorkManager {
|
||||
]
|
||||
]
|
||||
]
|
||||
guard netWorkStatu != .notReachable else {return}
|
||||
//guard netWorkStatu != .notReachable else {return}
|
||||
requestPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in
|
||||
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请求
|
||||
MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonPlayer.self) { [weak self] (response) in
|
||||
guard let self = self else {return}
|
||||
@ -636,7 +654,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(lyrics)
|
||||
}
|
||||
@ -683,7 +701,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(results)
|
||||
}
|
||||
@ -736,7 +754,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(result)
|
||||
}
|
||||
@ -787,7 +805,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(result)
|
||||
}
|
||||
@ -844,7 +862,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(result)
|
||||
}
|
||||
@ -901,7 +919,7 @@ extension MP_NetWorkManager {
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"clientVersion": "1.\(currTimeDate).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"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
|
||||
completion(result)
|
||||
}
|
||||
@ -1203,15 +1221,15 @@ extension MP_NetWorkManager {
|
||||
/// - Parameters:
|
||||
/// - player: player库
|
||||
/// - 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]?
|
||||
//解析player,获取资源库和信息库
|
||||
if let videoDetails = player.videoDetails {
|
||||
infos = parsingPlayerVideoDetails(videoDetails)
|
||||
}
|
||||
if let streamingData = player.streamingData {
|
||||
parsingPlayerStreamingData(streamingData){ audios,videos in
|
||||
completion((audios,videos),infos)
|
||||
parsingPlayerStreamingData(streamingData){ videos,floats,approxDurationMs in
|
||||
completion((videos,floats,approxDurationMs),infos)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1221,10 +1239,11 @@ extension MP_NetWorkManager {
|
||||
/// - Parameters:
|
||||
/// - streamingData: 资源库
|
||||
/// - 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 videos:[String] = []
|
||||
var audios:[String] = []
|
||||
var floats:[Float] = []
|
||||
var approxDurationMs:[String] = []
|
||||
let allFormats = (streamingData.formats ?? []) + (streamingData.adaptiveFormats ?? [])
|
||||
for format in allFormats {
|
||||
if let signatureCipher = format.signatureCipher {
|
||||
@ -1232,21 +1251,17 @@ extension MP_NetWorkManager {
|
||||
group?.enter()
|
||||
//获得资源签名,开始解密签名内容
|
||||
parsingPlayerSignatureCipher(signatureCipher) { result in
|
||||
//对数据进行拆分,分为视频资源和音频资源
|
||||
if format.mimeType?.contains("video") == true {
|
||||
//这是条视频资源
|
||||
videos.append(result)
|
||||
}
|
||||
if format.mimeType?.contains("audio") == true {
|
||||
audios.append(result)
|
||||
}
|
||||
//这是条视频资源
|
||||
videos.append(result)
|
||||
floats.append(format.bitrate ?? 0)
|
||||
approxDurationMs.append(format.approxDurationMs ?? "")
|
||||
// 离开DispatchGroup,表示异步任务完成
|
||||
group?.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
group?.notify(queue: .main) {
|
||||
completion(audios,videos)
|
||||
completion(videos, floats, approxDurationMs)
|
||||
group = nil
|
||||
}
|
||||
}
|
||||
@ -1270,11 +1285,11 @@ extension MP_NetWorkManager {
|
||||
//提取URl路径
|
||||
let urlSubstring = originalURLString[urlStartIndex...] // 从 &url= 之后开始提取
|
||||
let signString = String(originalURLString[sRange.upperBound..<spSigRange.lowerBound])
|
||||
|
||||
//加密的权限请求解码
|
||||
MP_WebWork.shared.excuteJavaScript(signString) { result in
|
||||
//与权限拼接
|
||||
let abString = urlSubstring + "&sig=" + result
|
||||
// print("Resources-SignatureDecryption:\(abString)")
|
||||
completion(abString)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import UIKit
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
import AVKit
|
||||
import FreeStreamer
|
||||
///播放器播放状态
|
||||
enum MP_PlayerStateType:Int {
|
||||
///未启动
|
||||
@ -43,13 +44,20 @@ typealias MP_PlayTimerStopAction = () -> Void
|
||||
///播放器调整进度时执行事件
|
||||
typealias MP_PlayTimerEditEndAction = () -> Void
|
||||
///播放器缓存值执行事件
|
||||
typealias MP_PlayCacheValueAction = (_ currentValue:TimeInterval, _ duration:TimeInterval) -> Void
|
||||
typealias MP_PlayCacheValueAction = (Float) -> Void
|
||||
///播放器
|
||||
class MP_PlayerManager:NSObject{
|
||||
///控制器单例
|
||||
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模块
|
||||
var loadPlayer:MPPositive_PlayerLoadViewModel!{
|
||||
didSet{
|
||||
@ -68,6 +76,7 @@ class MP_PlayerManager:NSObject{
|
||||
didSet{
|
||||
//当播放器状态发生变化时,对播放器按钮状态进行切换
|
||||
NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState)
|
||||
|
||||
}
|
||||
}
|
||||
///获取播放器播放状态
|
||||
@ -95,22 +104,73 @@ class MP_PlayerManager:NSObject{
|
||||
}
|
||||
///播放器启动时执行事件记录
|
||||
private var startActionBlock:MP_PlayTimerStartAction!
|
||||
|
||||
///播放器运行时执行事件记录
|
||||
var runActionBlock:MP_PlayTimerRunAction!
|
||||
///播放器缓存值闭包
|
||||
var cacheValueBlock:MP_PlayCacheValueAction!
|
||||
private override init() {
|
||||
super.init()
|
||||
// 添加观察者,监听播放结束事件
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
|
||||
// player.automaticallyWaitsToMinimizeStalling = false
|
||||
//// 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(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 {
|
||||
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:
|
||||
/// - startAction: 开始播放时需要执行的事件
|
||||
@ -122,143 +182,102 @@ class MP_PlayerManager:NSObject{
|
||||
print("Player No Data")
|
||||
return
|
||||
}
|
||||
//检索播放器状态
|
||||
switch playState {
|
||||
case .Null://未启动
|
||||
break
|
||||
case .Playing://启动中
|
||||
player.pause()
|
||||
case .Pause://暂停中
|
||||
break
|
||||
}
|
||||
//清除旧流媒体
|
||||
stopAndReleaseStream(&player)
|
||||
//记录事件
|
||||
if startAction != nil {
|
||||
startActionBlock = startAction
|
||||
}
|
||||
//覆盖播放器原有的playerItem
|
||||
player.replaceCurrentItem(with: loadPlayer.currentVideo.resourcePlayerItem)
|
||||
//将进度回归为0
|
||||
player.seek(to: .zero)
|
||||
//设置一个秒为刻度的时间值
|
||||
let interval:CMTime = .init(seconds: 1, preferredTimescale: .init(1))
|
||||
//为播放器添加运行时主线程每秒触发事件
|
||||
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
|
||||
if next != nil, (next.url == (loadPlayer.currentVideo.resourcePlayerURL! as NSURL)) {
|
||||
player = next
|
||||
}else {
|
||||
//配置当前播放音乐
|
||||
player = .init(url: loadPlayer.currentVideo.resourcePlayerURL!)
|
||||
player.maxRetryCount = 3
|
||||
}
|
||||
}
|
||||
|
||||
//实现KVO监听
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard let keyPath = keyPath else {
|
||||
return
|
||||
//预加载下一首(假如有的话)
|
||||
let index = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo) ?? 0
|
||||
if (loadPlayer.listViewVideos.count-1) > index {
|
||||
stopAndReleaseStream(&next)
|
||||
//纯在下一首,获取下一位的URL
|
||||
let nextURL = loadPlayer.listViewVideos[index + 1].resourcePlayerURL
|
||||
next = preloadNext(nextURL!)
|
||||
}
|
||||
//根据keyPath检索
|
||||
switch keyPath {
|
||||
case "status"://playerItem状态
|
||||
if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue {
|
||||
//判断当前播放器是否在播放当前音乐中
|
||||
if playState != .Playing {
|
||||
//当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
||||
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 已经准备好播放")
|
||||
}
|
||||
}else {
|
||||
print("当前音乐-\(loadPlayer.currentVideo?.title ?? "") 未做好准备播放,失败原因是\(loadPlayer.currentVideo?.resourcePlayerItem.error?.localizedDescription ?? "")")
|
||||
//资源更新,重新配置一下相关内容
|
||||
loadPlayer.remakeImproveData {
|
||||
[weak self] in
|
||||
//开始播放
|
||||
player.play()
|
||||
//获取播放器状态
|
||||
player.onStateChange = {
|
||||
[weak self] status in
|
||||
guard let self = self, findTurePlayer(player) else {return}
|
||||
switch status {
|
||||
case .fsAudioStreamFailed://加载失败
|
||||
print("\(loadPlayer.currentVideo?.title ?? "")加载失败")
|
||||
case .fsAudioStreamRetryingFailed://重试都失败了
|
||||
print("\(loadPlayer.currentVideo?.title ?? "")重试失败")
|
||||
print("失败URL:\(String(describing: loadPlayer.currentVideo?.resourcePlayerURL))")
|
||||
//重新获取资源
|
||||
loadPlayer.remakeImproveData { [weak self] in
|
||||
guard let self = self else {return}
|
||||
//重新播放
|
||||
play()
|
||||
//配置当前播放音乐
|
||||
player?.url = loadPlayer.currentVideo.resourcePlayerURL! as NSURL
|
||||
}
|
||||
}
|
||||
case "loadedTimeRanges"://当前缓冲进度
|
||||
//获取当前播放Item的缓冲值组
|
||||
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 {
|
||||
case .fsAudioStreamPlaying://加载成功
|
||||
//开始播放/正在播放
|
||||
print("\(loadPlayer.currentVideo?.title ?? "")开始播放")
|
||||
if playState != .Playing {
|
||||
//还未播放当前音乐,启动播放
|
||||
player.play()
|
||||
playState = .Playing
|
||||
//执行开始播放闭包
|
||||
if startActionBlock != nil {
|
||||
startActionBlock!()
|
||||
if startAction != nil {
|
||||
startAction!()
|
||||
}
|
||||
}
|
||||
}else {
|
||||
//没有足够的数据支持播放
|
||||
player.pause()
|
||||
playState = .Null
|
||||
case .fsAudioStreamPlaybackCompleted://播放完成
|
||||
playerDidFinishPlaying()
|
||||
case .fsAudioStreamEndOfFile://加载完成
|
||||
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 {
|
||||
return CMTimeGetSeconds(player.currentItem?.duration ?? .zero)
|
||||
|
||||
///预加载下一首流
|
||||
private func preloadNext(_ url:URL) -> FSAudioStream{
|
||||
let stream = FSAudioStream(url: url)
|
||||
stream?.maxRetryCount = 1
|
||||
// 开始预加载数据
|
||||
stream!.preload()
|
||||
print("下一首已经在预加载")
|
||||
return stream!
|
||||
}
|
||||
|
||||
|
||||
|
||||
//MARK: - 音乐播放结束
|
||||
//当前音乐播放结束时
|
||||
@objc private func playerDidFinishPlaying(_ sender:Notification) {
|
||||
@objc private func playerDidFinishPlaying() {
|
||||
//检索播放器对象
|
||||
guard playState == .Playing else {
|
||||
return
|
||||
}
|
||||
switch playType {
|
||||
case .single:
|
||||
var postion = FSStreamPosition()
|
||||
postion.position = 0
|
||||
//重播
|
||||
player.seek(to: CMTime.zero)
|
||||
player.seek(to: postion)
|
||||
player.play()
|
||||
default:
|
||||
//当前音乐播放器正在播放中,下一首
|
||||
@ -310,7 +329,8 @@ class MP_PlayerManager:NSObject{
|
||||
resumeAction!()
|
||||
}
|
||||
//继续播放器
|
||||
player.play()
|
||||
// player.play()
|
||||
player.pause()
|
||||
//切换播放器状态
|
||||
playState = .Playing
|
||||
}
|
||||
@ -323,7 +343,8 @@ class MP_PlayerManager:NSObject{
|
||||
return
|
||||
}
|
||||
//继续播放器
|
||||
player.play()
|
||||
// player.play()
|
||||
player.pause()
|
||||
//切换播放器状态
|
||||
playState = .Playing
|
||||
}
|
||||
@ -337,7 +358,7 @@ class MP_PlayerManager:NSObject{
|
||||
print("Player is not started")
|
||||
return
|
||||
}
|
||||
player.pause()
|
||||
player.stop()
|
||||
playState = .Null
|
||||
}
|
||||
//MARK: - 切歌(上一首/下一首)
|
||||
@ -430,21 +451,21 @@ class MP_PlayerManager:NSObject{
|
||||
@objc private func userSwitchCurrentVideoAction(_ sender:Notification) {
|
||||
//将播放器状态调整未播放
|
||||
playState = .Null
|
||||
//暂停播放
|
||||
player.pause()
|
||||
//优先获取传递的值
|
||||
if let video = sender.object as? MPPositive_SongViewModel {
|
||||
//切歌时移除KVO监听
|
||||
video.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
|
||||
video.resourcePlayerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
|
||||
video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
|
||||
//清理所有的流
|
||||
if player != nil {
|
||||
//清除所有流
|
||||
stopAndReleaseStream(&player)
|
||||
}
|
||||
if cacheValueBlock != nil {
|
||||
cacheValueBlock!(0, 1)
|
||||
cacheValueBlock!(0)
|
||||
}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
//开始播放
|
||||
play(startAction: startActionBlock)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
//开始播放
|
||||
play(startAction: startActionBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -460,22 +481,85 @@ class MP_PlayerManager:NSObject{
|
||||
/// - Parameters:
|
||||
/// - progress: 要调整进度值(保证在0-1范围内,超出该方法不会响应)
|
||||
func setEditProgressEnd(_ progress:Float, endAction:MP_PlayTimerEditEndAction? = nil) {
|
||||
guard playState != .Null else {
|
||||
guard playState != .Null, let player = player, findTurePlayer(player) else {
|
||||
return
|
||||
}
|
||||
guard progress >= 0, progress <= 1 else {
|
||||
return
|
||||
}
|
||||
//根据当前进度值设置时间节点
|
||||
let timePoint:Double = Double(progress)*getMusicDuration()
|
||||
//设置对应的时间值
|
||||
let time:CMTime = .init(seconds: timePoint, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
//调整播放器时间
|
||||
player.seek(to: time)
|
||||
//恢复播放
|
||||
resume()
|
||||
if endAction != nil {
|
||||
endAction!()
|
||||
var time:FSStreamPosition = .init()
|
||||
time.position = progress
|
||||
//获取当前值的大小
|
||||
let currentTime = player.currentTimePlayed.playbackTimeInSeconds
|
||||
if progress != currentTime {
|
||||
//调整播放器时间
|
||||
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)")
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
|
||||
@ -46,23 +46,31 @@ struct JsonPlayer: Codable {
|
||||
struct Format: Codable {
|
||||
///格式标签
|
||||
let itag: Int?
|
||||
///比特率
|
||||
let bitrate:Float?
|
||||
///格式编码
|
||||
let mimeType:String?
|
||||
///格式名
|
||||
let qualityLabel:String?
|
||||
///资源地址(双加密)
|
||||
let signatureCipher:String?
|
||||
///总长度
|
||||
let approxDurationMs:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case itag = "itag"
|
||||
case bitrate = "bitrate"
|
||||
case mimeType = "mimeType"
|
||||
case qualityLabel = "qualityLabel"
|
||||
case signatureCipher = "signatureCipher"
|
||||
case approxDurationMs = "approxDurationMs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
itag = try values.decodeIfPresent(Int.self, forKey: .itag)
|
||||
bitrate = try values.decodeIfPresent(Float.self, forKey: .bitrate)
|
||||
mimeType = try values.decodeIfPresent(String.self, forKey: .mimeType)
|
||||
qualityLabel = try values.decodeIfPresent(String.self, forKey: .qualityLabel)
|
||||
approxDurationMs = try values.decodeIfPresent(String.self, forKey: .approxDurationMs)
|
||||
signatureCipher = try values.decodeIfPresent(String.self, forKey: .signatureCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,10 @@ class MPPositive_CollectionListModel: NSManagedObject, MP_CoreDataManageableDele
|
||||
typealias ManagedObject = MPPositive_CollectionListModel
|
||||
///封面
|
||||
@NSManaged var coverImage:URL!
|
||||
///标题(单曲标题)
|
||||
///标题
|
||||
@NSManaged var title:String?
|
||||
///副标题
|
||||
@NSManaged var subtitle:String?
|
||||
///列表专辑预览ID
|
||||
@NSManaged var browseId:String?
|
||||
///列表专辑预览参数
|
||||
|
||||
@ -19,6 +19,8 @@ class MPPositive_CollectionSongModel: NSManagedObject, MP_CoreDataManageableDele
|
||||
@NSManaged var subtitle:String?
|
||||
///播放的VideoID
|
||||
@NSManaged var videoId:String?
|
||||
|
||||
|
||||
///歌词ID
|
||||
@NSManaged var lyricsID:String?
|
||||
///相关内容ID
|
||||
@NSManaged var relatedID:String?
|
||||
}
|
||||
|
||||
@ -11,12 +11,10 @@ import CoreData
|
||||
@objc(MPPositive_DownloadItemModel)
|
||||
class MPPositive_DownloadItemModel: NSManagedObject, MP_CoreDataManageableDelegate, MP_CoreDataOperationDelegate {
|
||||
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?
|
||||
///长文本标题(作者/播放次数/点赞次数)
|
||||
@ -24,9 +22,11 @@ class MPPositive_DownloadItemModel: NSManagedObject, MP_CoreDataManageableDelega
|
||||
///单曲长度文本(歌曲长度)
|
||||
@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
|
||||
@NSManaged var videoId:String!
|
||||
///相关内容ID
|
||||
|
||||
@ -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!
|
||||
}
|
||||
@ -10,11 +10,9 @@ import UIKit
|
||||
class MPPositive_SongItemModel: NSObject {
|
||||
///序列号(在当前列表中的排序)
|
||||
var index:Int!
|
||||
///视频源路径(等级制,默认取第一条最低质量)
|
||||
var resourceUrls:[String]?
|
||||
///音频资源路径(等级制,默认取第一条最低质量)
|
||||
var audioUrls:[String]?
|
||||
///封面路径(默认拿最后一条最清晰)
|
||||
///视频源路径组(等级制,默认取第一条最低质量)
|
||||
var resourceUrls:[String]?
|
||||
///封面路径组(默认拿最后一条最清晰)
|
||||
var coverUrls:[String]?
|
||||
///预览图片(默认拿最后一条最清晰)
|
||||
var reviewUrls:[String]?
|
||||
@ -28,9 +26,16 @@ class MPPositive_SongItemModel: NSObject {
|
||||
var shortBylineText:String?
|
||||
///歌词ID
|
||||
var lyricsID:String?
|
||||
///歌词
|
||||
var lyrics:String?
|
||||
///播放的VideoID
|
||||
var videoId:String!
|
||||
///相关内容ID
|
||||
var relatedID:String!
|
||||
|
||||
///比特率
|
||||
var bitrates:[Float]?
|
||||
///总长度
|
||||
var approxDurationMs:[String]?
|
||||
///列表ID
|
||||
var playlistId:String?
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,13 +8,20 @@
|
||||
import UIKit
|
||||
import AVKit
|
||||
import AVFoundation
|
||||
import FreeStreamer
|
||||
class MPPositive_SongViewModel: NSObject {
|
||||
///排序号
|
||||
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?
|
||||
///标题
|
||||
@ -31,31 +38,28 @@ class MPPositive_SongViewModel: NSObject {
|
||||
var isDlownd:Bool?
|
||||
///音乐实体
|
||||
var song:MPPositive_SongItemModel!
|
||||
///是否进行过预加载
|
||||
var isPloading:Bool = false
|
||||
// 标记为已取消
|
||||
private var isCancelled = false
|
||||
init(_ song:MPPositive_SongItemModel) {
|
||||
super.init()
|
||||
self.song = song
|
||||
// resourcePlayerItem = nil
|
||||
configure()
|
||||
}
|
||||
deinit {
|
||||
//释放内存
|
||||
resourcePlayerItem = nil
|
||||
resourceAsset = nil
|
||||
isCancelled = true
|
||||
print("\(title ?? "")被释放了")
|
||||
resourcePlayerURL = nil
|
||||
}
|
||||
//数据配置
|
||||
private func configure() {
|
||||
func configure() {
|
||||
reloadCollectionAndDownLoad()
|
||||
index = song.index
|
||||
//资源路径默认取第一条
|
||||
if song.resourceUrls?.first != nil, let first = URL(string: song.resourceUrls?.first ?? ""){
|
||||
//创建一个资产链接并允许它预加载
|
||||
resourceAsset = .init(url: first, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
|
||||
//创建PlayerItem
|
||||
resourcePlayerItem = .init(asset: resourceAsset)
|
||||
|
||||
if let first = song.resourceUrls?.first {
|
||||
//判断是否下载
|
||||
if isDlownd == true {
|
||||
resourcePlayerURL = .init(string:first)
|
||||
}else {
|
||||
//没有完成下载,使用网络路径
|
||||
resourcePlayerURL = .init(string: first)
|
||||
}
|
||||
}
|
||||
//封面路径默认取最后一条
|
||||
if song.reviewUrls?.first != nil {
|
||||
@ -70,21 +74,21 @@ class MPPositive_SongViewModel: NSObject {
|
||||
subtitle = song.shortBylineText!
|
||||
}
|
||||
//歌词
|
||||
if song.lyricsID != nil {
|
||||
//执行网络请求拿到歌词数据
|
||||
MP_NetWorkManager.shared.requestLyric(song.lyricsID!) {[weak self] lyrics in
|
||||
self?.lyrics = lyrics
|
||||
if song.lyrics != nil {
|
||||
lyrics = song.lyrics
|
||||
}else {
|
||||
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 {
|
||||
relatedId = song.relatedID
|
||||
}
|
||||
reloadCollectionAndDownLoad()
|
||||
//执行预加载
|
||||
// if isPloading == false {
|
||||
// preloadAsset(resourceAsset)
|
||||
// }
|
||||
}
|
||||
//页面状态更新
|
||||
func reloadCollectionAndDownLoad() {
|
||||
@ -94,66 +98,4 @@ class MPPositive_SongViewModel: NSObject {
|
||||
//检索是否下载
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,7 +43,7 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
self.listViewVideos = []
|
||||
}
|
||||
|
||||
///将选中Video的上下2项包括本身总计3项Video进行补全转为ViewModel,并播放这首音乐
|
||||
///将选中Video的上下1项包括本身总计3项Video进行补全转为ViewModel,并播放这首音乐
|
||||
func improveData(_ targetVideoId:String, isRandom:Bool = false) {
|
||||
//对于选中Video的集合
|
||||
var array:[MPPositive_SongItemModel] = []
|
||||
@ -82,25 +82,41 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
let videoIDs = Set(listViewVideos.map({$0.song.videoId}))
|
||||
//比较videoID,去掉已经补完的内容
|
||||
array = array.filter({!videoIDs.contains($0.videoId)})
|
||||
|
||||
group = DispatchGroup()
|
||||
//去重完毕,对剩下内容补完
|
||||
for item in array {
|
||||
group?.enter()
|
||||
//补全歌词id和相关内容id
|
||||
improveDataforLycirsAndRelated(item) {[weak self] (result) in
|
||||
item.lyricsID = result.0
|
||||
item.relatedID = result.1
|
||||
self?.group?.leave()
|
||||
if item.lyricsID == nil || item.relatedID == nil {
|
||||
improveDataforLycirsAndRelated(item) {[weak self] (result) in
|
||||
item.lyricsID = result.0
|
||||
item.relatedID = result.1
|
||||
self?.group?.leave()
|
||||
}
|
||||
}else {
|
||||
group?.leave()
|
||||
}
|
||||
group?.enter()
|
||||
//补全资源路径组和封面路径组
|
||||
improveDataforResouceAndCover(item) {[weak self] resourceUrls, coverUrls in
|
||||
item.resourceUrls = resourceUrls.1
|
||||
item.audioUrls = resourceUrls.0
|
||||
item.coverUrls = coverUrls
|
||||
//判断当前videoID是否进行过下载
|
||||
if let resource = getDocumentsFileURL(item.videoId) {
|
||||
//下载过,resource直接填入
|
||||
item.resourceUrls = [resource]
|
||||
//补全完成,转化为ViewModel,并添加进listViewVideos
|
||||
self?.listViewVideos.append(.init(item))
|
||||
self?.group?.leave()
|
||||
listViewVideos.append(.init(item))
|
||||
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
|
||||
//补全完成,转化为ViewModel,并添加进listViewVideos
|
||||
self?.listViewVideos.append(.init(item))
|
||||
self?.group?.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
group?.notify(queue: .main, execute: {
|
||||
@ -117,17 +133,21 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
//当前歌曲不能播放,需要重新配置资源
|
||||
improveDataforResouceAndCover(currentVideo.song) {[weak self] resourceUrls, coverUrls in
|
||||
guard let self = self else {return}
|
||||
currentVideo.song.resourceUrls = resourceUrls.1
|
||||
currentVideo.song.audioUrls = resourceUrls.0
|
||||
currentVideo.song.resourceUrls = resourceUrls.0
|
||||
currentVideo.song.bitrates = resourceUrls.1
|
||||
currentVideo.song.approxDurationMs = resourceUrls.2
|
||||
//成功更新资源,将重新补完的歌曲,放进listViewVideos中
|
||||
listViewVideos.forEach({ item in
|
||||
if item.song.videoId == self.currentVideo.song.videoId {
|
||||
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.resourcePlayerItem = .init(asset: currentVideo.resourceAsset!)
|
||||
// currentVideo.resourceAsset = .init(url: .init(string: currentVideo.song.resourceUrls!.first!)!)
|
||||
// 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
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
|
||||
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
|
||||
}
|
||||
|
||||
///调用next对单曲数据歌词ID与相关ID补全
|
||||
private func improveDataforLycirsAndRelated(_ song:MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) {
|
||||
//单曲补全需要再次调用next接口
|
||||
@ -164,9 +189,9 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
}
|
||||
}
|
||||
///调用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接口
|
||||
MP_NetWorkManager.shared.requestPlayer(song) { resourceUrls, coverUrls in
|
||||
MP_NetWorkManager.shared.requestPlayer(song.videoId, playlistId: song.playlistId ?? "") { resourceUrls, coverUrls in
|
||||
completion(resourceUrls,coverUrls)
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,12 @@ class MPPositive_SearchResultsLoadViewModel: NSObject {
|
||||
}
|
||||
//根据用户输入文本内容请求搜索接口
|
||||
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
|
||||
self?.sectionLists = results
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ class MPPositive_NavigationController: MP_NavigationController {
|
||||
//每一次push都会执行这个方法,push之前设置viewController的hidesBottomBarWhenPushed
|
||||
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||
viewController.hidesBottomBarWhenPushed = true
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_nav_push)
|
||||
super.pushViewController(viewController, animated: true)
|
||||
viewController.hidesBottomBarWhenPushed = false
|
||||
}
|
||||
@ -30,13 +31,14 @@ class MPPositive_NavigationController: MP_NavigationController {
|
||||
let count = self.children.count-2
|
||||
let controller = self.children[count]
|
||||
controller.hidesBottomBarWhenPushed = true
|
||||
NotificationCenter.notificationKey.post(notificationName: .sideA_hidden_show)
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_nav_push)
|
||||
return super.popViewController(animated: true)
|
||||
}
|
||||
//如果viewController栈中存在的ViewController的个数为两个,再返回上一级界面就是根界面了
|
||||
//那么要对tabbar进行显示
|
||||
let controller:UIViewController = self.children[0]
|
||||
controller.hidesBottomBarWhenPushed = false
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_nav_pop)
|
||||
return super.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,14 @@ class MPPositive_TabBarController: UITabBarController, UIViewControllerTransitio
|
||||
//自定义tabBar
|
||||
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 var isbottomShow:Bool = false{
|
||||
willSet{
|
||||
if isbottomShow != newValue {
|
||||
switch_bottomShowAnimation(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
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(bottomAnimationAction(_:)), notificationName: .pup_bottom_show)
|
||||
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 {
|
||||
//移除所有监听
|
||||
@ -84,7 +94,7 @@ extension MPPositive_TabBarController {
|
||||
let playerVC = MPPositive_PlayerViewController()
|
||||
playerVC.modalPresentationStyle = .fullScreen
|
||||
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?.present(playerVC, animated: true)
|
||||
@ -93,7 +103,7 @@ extension MPPositive_TabBarController {
|
||||
}
|
||||
//切换底部音乐模块状态
|
||||
@objc private func bottomAnimationAction(_ sender:Notification) {
|
||||
switch_bottomShowAnimation(MP_PlayerManager.shared.loadPlayer != nil)
|
||||
isbottomShow = MP_PlayerManager.shared.loadPlayer != nil
|
||||
}
|
||||
//底部BottomView的切换动画
|
||||
private func switch_bottomShowAnimation(_ state:Bool) {
|
||||
@ -109,4 +119,27 @@ extension MPPositive_TabBarController {
|
||||
}
|
||||
}
|
||||
}
|
||||
//页面push事件
|
||||
@objc private func pushAction(_ sender:Notification) {
|
||||
//检索页面状态
|
||||
if isbottomShow == true {
|
||||
//将bottomView,向下移动83
|
||||
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 {
|
||||
//将bottomView,向上移动83
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
[weak self] in
|
||||
guard let self = self else { return }
|
||||
bottomView.transform = .init(translationX: 0, y: -145*width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,23 +7,219 @@
|
||||
|
||||
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() {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
//返回顶部事件View组
|
||||
private func showTopView() -> UIView {
|
||||
let topView = UIView()
|
||||
topView.backgroundColor = .clear
|
||||
//添加事件按钮
|
||||
let first = actionView(artistsLabel, text: "Love_Artists_logo", tag: 0)
|
||||
topView.addSubview(first)
|
||||
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)
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -178,7 +178,7 @@ extension MPPositive_ArtistShowViewController: JXPagingViewDelegate{
|
||||
navigationController?.pushViewController(artistVC, animated: true)
|
||||
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)
|
||||
case .single:
|
||||
//单曲/视频跳转
|
||||
|
||||
@ -31,6 +31,7 @@ class MPPositive_HomeViewController: MPPositive_BaseViewController{
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(MPPositive_HomeShowTableViewCell.self, forCellReuseIdentifier: MPPositive_HomeShowTableViewCellID)
|
||||
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
|
||||
return tableView
|
||||
}()
|
||||
private let MPPositive_HomeShowTableViewCellID = "MPPositive_HomeShowTableViewCell"
|
||||
@ -90,7 +91,17 @@ extension MPPositive_HomeViewController: UITableViewDataSource, UITableViewDeleg
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_HomeShowTableViewCellID, for: indexPath) as! MPPositive_HomeShowTableViewCell
|
||||
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 = {
|
||||
[weak self] (item) in
|
||||
guard let self = self else {return}
|
||||
@ -108,7 +119,7 @@ extension MPPositive_HomeViewController: UITableViewDataSource, UITableViewDeleg
|
||||
}
|
||||
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)
|
||||
default:
|
||||
break
|
||||
|
||||
@ -64,6 +64,7 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(MPPositive_MusicItemShowTableViewCell.self, forCellReuseIdentifier: MPPositive_MusicItemShowTableViewCellID)
|
||||
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
|
||||
return tableView
|
||||
}()
|
||||
private let MPPositive_MusicItemShowTableViewCellID = "MPPositive_MusicItemShowTableViewCell"
|
||||
@ -81,16 +82,20 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
var browseid:String = ""
|
||||
var params:String = ""
|
||||
private var browseid:String!
|
||||
private var params:String!
|
||||
private var centerTtitle:String!
|
||||
private var subtitle:String!
|
||||
/// 生成列表控制器
|
||||
/// - Parameters:
|
||||
/// - browseId: 列表的id
|
||||
/// - params: 列表的编码
|
||||
init(_ browseId:String, params:String) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
init(_ browseId:String, params:String, title:String, subtitle:String) {
|
||||
self.browseid = browseId
|
||||
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
|
||||
guard let self = self else {return}
|
||||
@ -237,23 +242,24 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
|
||||
if self.collectionListBtn.isSelected == true{
|
||||
self.collectionListBtn.isSelected = false
|
||||
|
||||
MPPositive_CollectionListModel.fetch(.init(format: "browseId == %@", self.browseid)).forEach { i in
|
||||
if i.browseId == self.browseid{
|
||||
if i.params == self.params{
|
||||
MPPositive_CollectionListModel.delete(i)
|
||||
}
|
||||
MPPositive_CollectionListModel.fetch(.init(format: "browseId == %@", self.browseid)).forEach { i in
|
||||
if i.browseId == self.browseid{
|
||||
if i.params == self.params{
|
||||
MPPositive_CollectionListModel.delete(i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
MPPositive_LoadCoreModel.shared.reloadCollectionListViewModels(nil)
|
||||
}else{
|
||||
self.collectionListBtn.isSelected = true
|
||||
let item = MPPositive_CollectionListModel.create()
|
||||
item.title = listOrAlbum.header.title
|
||||
item.title = self.centerTtitle
|
||||
item.subtitle = self.subtitle
|
||||
item.browseId = self.browseid
|
||||
item.params = self.params
|
||||
item.coverImage = listOrAlbum.header.coverUrl
|
||||
MPPositive_CollectionListModel.save()
|
||||
|
||||
MPPositive_LoadCoreModel.shared.reloadCollectionListViewModels(nil)
|
||||
}
|
||||
}
|
||||
//更改当前列表播放状态
|
||||
|
||||
@ -45,6 +45,7 @@ class MPPositive_MoreContentViewController: MPPositive_BaseViewController {
|
||||
collectionView.delegate = self
|
||||
collectionView.register(MPPositive_MoreListContentCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_MoreListContentCollectionViewCellID)
|
||||
collectionView.register(MPPositive_HomeListFifthCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_HomeListFifthCollectionViewCellID)
|
||||
collectionView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
|
||||
return collectionView
|
||||
}()
|
||||
//列表cell
|
||||
|
||||
@ -21,6 +21,7 @@ class MPPositive_PlayerListShowViewController: UIViewController {
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(MPPositive_PlayerListShowTableViewCell.self, forCellReuseIdentifier: MPPositive_PlayerListShowTableViewCellID)
|
||||
|
||||
return tableView
|
||||
}()
|
||||
private let MPPositive_PlayerListShowTableViewCellID = "MPPositive_PlayerListShowTableViewCell"
|
||||
|
||||
@ -133,14 +133,11 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
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 }
|
||||
if value < duration {
|
||||
//进度缓存中
|
||||
let float = value/duration
|
||||
coverView.progressView.setProgress(Float(float), animated: false)
|
||||
if progress <= 1 {
|
||||
coverView.progressView.setProgress(progress, animated: false)
|
||||
}else {
|
||||
//进度缓存满了
|
||||
coverView.progressView.setProgress(1, animated: false)
|
||||
}
|
||||
}
|
||||
@ -165,7 +162,6 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
if MP_PlayerManager.shared.loadPlayer.currentVideo != nil {
|
||||
uploadUI()
|
||||
}
|
||||
coverView.restoreDownloadProgress()
|
||||
}
|
||||
//视图配置
|
||||
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
|
||||
coverView.loadBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isDlownd ?? false
|
||||
coverView.collectionSongBtn.isSelected = MP_PlayerManager.shared.loadPlayer.currentVideo?.isCollection ?? false
|
||||
coverView.restoreDownloadProgress()
|
||||
}
|
||||
//MARK: - 通知
|
||||
//播放器音乐刷新
|
||||
|
||||
@ -51,6 +51,7 @@ class MPPositive_RecommendViewController: MPPositive_BaseViewController {
|
||||
collectionView.dataSource = self
|
||||
collectionView.delegate = self
|
||||
collectionView.register(MPPositive_RecommendMemberCollectionViewCell.self, forCellWithReuseIdentifier: MPPositive_RecommendMemberCollectionViewCellID)
|
||||
|
||||
return collectionView
|
||||
}()
|
||||
private let MPPositive_RecommendMemberCollectionViewCellID = "MPPositive_RecommendMemberCollectionViewCell"
|
||||
@ -190,7 +191,7 @@ extension MPPositive_RecommendViewController: JXSegmentedListContainerViewDataSo
|
||||
switch item.browseItem.itemType {
|
||||
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)
|
||||
case .single:
|
||||
//单曲/视频跳转
|
||||
|
||||
@ -61,6 +61,21 @@ class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
|
||||
private lazy var suggestionView:MPPositive_SearchSuggestionsView = .init(frame: .zero)
|
||||
//MARK: - 结果展示View
|
||||
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() {
|
||||
super.viewDidLoad()
|
||||
setTitle("Result")
|
||||
@ -88,7 +103,7 @@ class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
|
||||
navigationController?.pushViewController(artistVC, animated: true)
|
||||
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)
|
||||
case .single:
|
||||
//单曲/视频跳转
|
||||
|
||||
@ -14,6 +14,28 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
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() {
|
||||
super.viewDidLoad()
|
||||
setTitle("")
|
||||
@ -22,6 +44,7 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
||||
}
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
collectionView.reloadData()
|
||||
}
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
@ -40,6 +63,23 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
||||
make.top.right.left.equalToSuperview()
|
||||
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{
|
||||
@ -71,5 +111,124 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
||||
let resultVC = MPPositive_SearchResultShowViewController()
|
||||
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()
|
||||
}
|
||||
//处理所有item的layoutAttributes
|
||||
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
|
||||
}
|
||||
//处理单个item的layoutAttributes
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
//起始x值
|
||||
var x = sectionInset.left
|
||||
var y = sectionInset.top
|
||||
//判断获得前一个cell的x和y
|
||||
let preRow = indexPath.row - 1
|
||||
if preRow >= 0 {
|
||||
//不是第一个cell
|
||||
if originyArray.count > preRow {
|
||||
//更新x和y值
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -102,13 +102,13 @@ class MPPositive_ArtistShowHeaderView: UIView {
|
||||
// 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 {
|
||||
if item.artistId == self.artistid {
|
||||
MPPositive_CollectionArtistModel.delete(item)
|
||||
}
|
||||
}
|
||||
|
||||
MPPositive_LoadCoreModel.shared.reloadCollectionArtistViewModels(nil)
|
||||
|
||||
}else{
|
||||
self.collectionBtn.isSelected = true
|
||||
@ -118,7 +118,7 @@ class MPPositive_ArtistShowHeaderView: UIView {
|
||||
item.subtitle = artist.header.subscriptionedText
|
||||
item.artistId = self.artistid
|
||||
MPPositive_CollectionArtistModel.save()
|
||||
|
||||
MPPositive_LoadCoreModel.shared.reloadCollectionArtistViewModels(nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ class MPPositive_ArtistShowTypeView: UIView, JXPagingViewListViewDelegate {
|
||||
tableView.register(MPPositive_ArtistShowSongTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowSongTableViewCellID)
|
||||
tableView.register(MPPositive_ArtistShowListableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowListableViewCellID)
|
||||
tableView.register(MPPositive_ArtistDescriptionTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistDescriptionTableViewCellID)
|
||||
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
|
||||
//添加一个上拉加载
|
||||
let footer = MJRefreshAutoGifFooter {
|
||||
[weak self] in
|
||||
|
||||
@ -28,13 +28,13 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell {
|
||||
return btn
|
||||
}()
|
||||
///下载状态按钮
|
||||
private lazy var loadBtn:UIButton = {
|
||||
let btn:UIButton = .init()
|
||||
btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
|
||||
btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
// private lazy var loadBtn:UIButton = {
|
||||
// let btn:UIButton = .init()
|
||||
// btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
|
||||
// btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
|
||||
// btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
|
||||
// return btn
|
||||
// }()
|
||||
//音乐实体
|
||||
var itemView:MPPositive_BrowseItemViewModel!{
|
||||
didSet{
|
||||
@ -81,17 +81,17 @@ class MPPositive_MusicItemShowTableViewCell: UITableViewCell {
|
||||
make.centerY.equalTo(coverImageView.snp.centerY)
|
||||
make.right.equalToSuperview().offset(-18*width)
|
||||
}
|
||||
contentView.addSubview(loadBtn)
|
||||
loadBtn.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(24*width)
|
||||
make.centerY.equalTo(coverImageView.snp.centerY)
|
||||
make.right.equalToSuperview().offset(-54*width)
|
||||
}
|
||||
// contentView.addSubview(loadBtn)
|
||||
// loadBtn.snp.makeConstraints { make in
|
||||
// make.width.height.equalTo(24*width)
|
||||
// make.centerY.equalTo(coverImageView.snp.centerY)
|
||||
// make.right.equalToSuperview().offset(-54*width)
|
||||
// }
|
||||
contentView.addSubview(titleLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(coverImageView.snp.top).offset(10*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)
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
|
||||
@ -51,6 +51,7 @@ class MPPositive_PlayerCoverView: UIView {
|
||||
progressView.isUserInteractionEnabled = true
|
||||
progressView.progressTintColor = .init(hex: "#FFFFFF", alpha: 0.3)
|
||||
progressView.trackTintColor = .clear
|
||||
progressView.progress = 0
|
||||
return progressView
|
||||
}()
|
||||
///当前播放时间值Label
|
||||
@ -72,24 +73,15 @@ class MPPositive_PlayerCoverView: UIView {
|
||||
}
|
||||
return maskView
|
||||
}()
|
||||
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
configure()
|
||||
|
||||
|
||||
|
||||
//添加监听
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkNotReachableAction(_:)), notificationName: .net_switch_notReachable)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(netWorkReachableAction(_:)), notificationName: .net_switch_reachable)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
// NotificationCenter.default.addObserver(self, selector: #selector(updateProgress(_:)), name: Notification.Name("DownloadProgressUpdated"), object: nil)
|
||||
@ -100,20 +92,26 @@ class MPPositive_PlayerCoverView: UIView {
|
||||
|
||||
|
||||
public func restoreDownloadProgress() {
|
||||
|
||||
if let currentVideo = MP_PlayerManager.shared.loadPlayer.currentVideo,
|
||||
let videoURLString = currentVideo.song.resourceUrls?.first,
|
||||
let videoURL = URL(string: videoURLString) {
|
||||
if let progress = DownloadManager.shared.getProgress(for: videoURL) {
|
||||
//判断当前是正在下载的VideoID
|
||||
addCircularProgressBar(over: loadBtn)
|
||||
loadView.setProgress(to: progress)
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // 延迟0.1秒
|
||||
self.layoutIfNeeded()
|
||||
self.loadBtn.setBackgroundImage(UIImage(named: ""), for: .normal)
|
||||
self.loadBtn.setImage(UIImage(named: "download"), for: .normal)
|
||||
self.addCircularProgressBar(over: self.loadBtn)
|
||||
self.loadView.setProgress(to: progress)
|
||||
// }
|
||||
self.addCircularProgressBar(over: self.loadBtn)
|
||||
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 {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
@ -257,6 +251,7 @@ class MPPositive_PlayerCoverView: UIView {
|
||||
}
|
||||
}
|
||||
MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad()
|
||||
MPPositive_LoadCoreModel.shared.reloadCollectionSongViewModel(nil)
|
||||
}
|
||||
}else{
|
||||
self.collectionSongBtn.isSelected = true
|
||||
@ -266,8 +261,11 @@ class MPPositive_PlayerCoverView: UIView {
|
||||
item.videoId = MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId
|
||||
item.subtitle = MP_PlayerManager.shared.loadPlayer.currentVideo.subtitle
|
||||
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()
|
||||
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,
|
||||
let videoURLString = currentVideo.song.resourceUrls?.first,
|
||||
let videoURL = URL(string: videoURLString) {
|
||||
let videoId = currentVideo.song.videoId ?? "default_video_id"
|
||||
DownloadManager.shared.downloadVideo(from: videoURL, videoId: videoId, progressHandler: { [weak self] progress in
|
||||
DownloadManager.shared.downloadVideo(from: videoURL, song: MP_PlayerManager.shared.loadPlayer.currentVideo.song, progressHandler: { [weak self] progress in
|
||||
DispatchQueue.main.async {
|
||||
self?.loadView.setProgress(to: progress)
|
||||
NotificationCenter.default.post(name: Notification.Name("DownloadProgressUpdated"), object: nil, userInfo: ["url": videoURL, "progress": progress])
|
||||
}
|
||||
}, completion: { [weak self] result in
|
||||
switch result {
|
||||
case .success(let fileURL):
|
||||
case .success(let song):
|
||||
let item = MPPositive_DownloadItemModel.create()
|
||||
item.resourcePath = "\(fileURL)"
|
||||
item.coverImage = URL(string: MP_PlayerManager.shared.loadPlayer.currentVideo.song.coverUrls!.first!)
|
||||
item.reviewImage = URL(string: MP_PlayerManager.shared.loadPlayer.currentVideo.song.reviewUrls!.first!)
|
||||
item.title = MP_PlayerManager.shared.loadPlayer.currentVideo.song.title
|
||||
item.longBylineText = MP_PlayerManager.shared.loadPlayer.currentVideo.song.longBylineText
|
||||
item.lengthText = MP_PlayerManager.shared.loadPlayer.currentVideo.song.lengthText
|
||||
item.shortBylineText = MP_PlayerManager.shared.loadPlayer.currentVideo.song.shortBylineText
|
||||
item.lyrics = MP_PlayerManager.shared.loadPlayer.currentVideo.lyrics
|
||||
item.videoId = MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId
|
||||
item.relatedID = MP_PlayerManager.shared.loadPlayer.currentVideo.song.relatedID
|
||||
//检索
|
||||
item.coverImage = song.coverUrls!.last
|
||||
item.reviewImage = song.reviewUrls!.last
|
||||
item.title = song.title
|
||||
item.longBylineText = song.longBylineText
|
||||
item.lengthText = song.lengthText
|
||||
item.shortBylineText = song.shortBylineText
|
||||
item.lyrics = song.lyrics
|
||||
item.lyricsID = song.lyricsID
|
||||
item.videoId = song.videoId
|
||||
item.relatedID = song.relatedID
|
||||
|
||||
MPPositive_DownloadItemModel.save()
|
||||
DispatchQueue.main.async {
|
||||
@ -308,6 +306,8 @@ class MPPositive_PlayerCoverView: UIView {
|
||||
MP_PlayerManager.shared.loadPlayer.currentVideo.reloadCollectionAndDownLoad()
|
||||
self?.loadBtn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .normal)
|
||||
self?.loadBtn.setImage(UIImage(named: ""), for: .normal)
|
||||
print("完成了对\(song.title ?? "")的下载")
|
||||
MPPositive_LoadCoreModel.shared.reloadLoadSongViewModel(nil)
|
||||
}
|
||||
case .failure(let error):
|
||||
print("Download failed with error: \(error)")
|
||||
|
||||
@ -22,6 +22,7 @@ class MPPositive_RecommendShowTypeView: UIView, JXSegmentedListContainerViewList
|
||||
tableView.delegate = self
|
||||
tableView.register(MPPositive_ArtistShowSongTableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowSongTableViewCellID)
|
||||
tableView.register(MPPositive_ArtistShowListableViewCell.self, forCellReuseIdentifier: MPPositive_ArtistShowListableViewCellID)
|
||||
tableView.contentInset = .init(top: 0, left: 0, bottom: 70*width, right: 0)
|
||||
return tableView
|
||||
}()
|
||||
private let MPPositive_ArtistShowSongTableViewCellID = "MPPositive_ArtistShowSongTableViewCell"
|
||||
|
||||
@ -21,6 +21,7 @@ class MPPositive_SearchResultPreviewShowView: UIView, JXSegmentedListContainerVi
|
||||
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"
|
||||
|
||||
@ -24,14 +24,14 @@ class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
|
||||
btn.addTarget(self, action: #selector(moreActionClick(_ :)), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
///下载状态按钮
|
||||
private lazy var loadBtn:UIButton = {
|
||||
let btn:UIButton = .init()
|
||||
btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
|
||||
btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
// ///下载状态按钮
|
||||
// private lazy var loadBtn:UIButton = {
|
||||
// let btn:UIButton = .init()
|
||||
// btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
|
||||
// btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
|
||||
// btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
|
||||
// return btn
|
||||
// }()
|
||||
var itemView:MPPositive_SearchResultItemViewModel!{
|
||||
didSet{
|
||||
itemView.setImage(iconImageView)
|
||||
@ -40,13 +40,28 @@ class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
|
||||
//检索类型
|
||||
if itemView.item.itemType == .single {
|
||||
moreBtn.isHidden = false
|
||||
loadBtn.isHidden = false
|
||||
// loadBtn.isHidden = false
|
||||
}else {
|
||||
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?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
@ -81,17 +96,17 @@ class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
|
||||
make.centerY.equalTo(iconImageView.snp.centerY)
|
||||
make.right.equalToSuperview().offset(-18*width)
|
||||
}
|
||||
contentView.addSubview(loadBtn)
|
||||
loadBtn.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(24*width)
|
||||
make.centerY.equalTo(iconImageView.snp.centerY)
|
||||
make.right.equalToSuperview().offset(-54*width)
|
||||
}
|
||||
// contentView.addSubview(loadBtn)
|
||||
// loadBtn.snp.makeConstraints { make in
|
||||
// make.width.height.equalTo(24*width)
|
||||
// make.centerY.equalTo(iconImageView.snp.centerY)
|
||||
// make.right.equalToSuperview().offset(-54*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(loadBtn.snp.left).offset(-10*width)
|
||||
make.right.equalTo(moreBtn.snp.left).offset(-10*width)
|
||||
}
|
||||
contentView.addSubview(subtitleLabel)
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
|
||||
@ -22,6 +22,7 @@ class MPPositive_SearchResultTypeShowView: UIView, JXSegmentedListContainerViewL
|
||||
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)
|
||||
//添加一个上拉加载
|
||||
let footer = MJRefreshAutoGifFooter {
|
||||
[weak self] in
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
5
Podfile
@ -23,5 +23,8 @@ pod 'JXSegmentedView'
|
||||
pod 'JXPagingView/Paging'
|
||||
#刷新支持
|
||||
pod 'MJRefresh'
|
||||
|
||||
#流音频播放
|
||||
pod 'FreeStreamer'
|
||||
#下载框架
|
||||
pod 'Tiercel'
|
||||
end
|
||||
|
||||
14
Podfile.lock
@ -1,18 +1,23 @@
|
||||
PODS:
|
||||
- Alamofire (5.9.1)
|
||||
- FreeStreamer (4.0.0):
|
||||
- Reachability (~> 3.0)
|
||||
- IQKeyboardManagerSwift (6.5.16)
|
||||
- JXPagingView/Paging (2.1.3)
|
||||
- JXSegmentedView (1.3.3)
|
||||
- Kingfisher (7.11.0)
|
||||
- MJRefresh (3.7.9)
|
||||
- Reachability (3.7.6)
|
||||
- SnapKit (5.7.1)
|
||||
- SVProgressHUD (2.3.1):
|
||||
- SVProgressHUD/Core (= 2.3.1)
|
||||
- SVProgressHUD/Core (2.3.1)
|
||||
- SwiftDate (6.3.1)
|
||||
- Tiercel (3.2.5)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- FreeStreamer
|
||||
- IQKeyboardManagerSwift
|
||||
- JXPagingView/Paging
|
||||
- JXSegmentedView
|
||||
@ -21,30 +26,37 @@ DEPENDENCIES:
|
||||
- SnapKit
|
||||
- SVProgressHUD
|
||||
- SwiftDate
|
||||
- Tiercel
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Alamofire
|
||||
- FreeStreamer
|
||||
- IQKeyboardManagerSwift
|
||||
- JXPagingView
|
||||
- JXSegmentedView
|
||||
- Kingfisher
|
||||
- MJRefresh
|
||||
- Reachability
|
||||
- SnapKit
|
||||
- SVProgressHUD
|
||||
- SwiftDate
|
||||
- Tiercel
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
||||
FreeStreamer: 7e9c976045701ac2f7e9c14c17245203c37bf2ea
|
||||
IQKeyboardManagerSwift: 12d89768845bb77b55cc092ecc2b1f9370f06b76
|
||||
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||
JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68
|
||||
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
Reachability: fd0ecd23705e2599e4cceeb943222ae02296cbc6
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
|
||||
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
||||
Tiercel: c0a73f876a72800333b15f4e7e48791f4ad21e90
|
||||
|
||||
PODFILE CHECKSUM: ba88795291c32ea83d380e5384537ca7f5568cd7
|
||||
PODFILE CHECKSUM: 3804949e23587f6d341ef21aa5e0b1c55a818968
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
274
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSAudioController.h
generated
Normal 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
|
||||
875
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSAudioController.m
generated
Normal 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
|
||||
600
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSAudioStream.h
generated
Normal 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
|
||||
1905
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSAudioStream.mm
generated
Normal file
128
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSCheckContentTypeRequest.h
generated
Normal 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
|
||||
236
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSCheckContentTypeRequest.m
generated
Normal 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
|
||||
71
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSParsePlaylistRequest.h
generated
Normal 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
|
||||
325
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSParsePlaylistRequest.m
generated
Normal 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
|
||||
28
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSParseRssPodcastFeedRequest.h
generated
Normal 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
|
||||
100
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSParseRssPodcastFeedRequest.m
generated
Normal 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
|
||||
41
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSPlaylistItem.h
generated
Normal 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
|
||||
25
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSPlaylistItem.m
generated
Normal 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
|
||||
112
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSXMLHttpRequest.h
generated
Normal 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
|
||||
255
Pods/FreeStreamer/FreeStreamer/FreeStreamer/FSXMLHttpRequest.m
generated
Normal 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
|
||||
566
Pods/FreeStreamer/FreeStreamer/FreeStreamer/audio_queue.cpp
generated
Normal 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
|
||||
102
Pods/FreeStreamer/FreeStreamer/FreeStreamer/audio_queue.h
generated
Normal 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
|
||||
2229
Pods/FreeStreamer/FreeStreamer/FreeStreamer/audio_stream.cpp
generated
Normal file
255
Pods/FreeStreamer/FreeStreamer/FreeStreamer/audio_stream.h
generated
Normal 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
|
||||
440
Pods/FreeStreamer/FreeStreamer/FreeStreamer/caching_stream.cpp
generated
Normal 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
|
||||
73
Pods/FreeStreamer/FreeStreamer/FreeStreamer/caching_stream.h
generated
Normal 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 */
|
||||
30
Pods/FreeStreamer/FreeStreamer/FreeStreamer/file_output.cpp
generated
Normal 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
|
||||
32
Pods/FreeStreamer/FreeStreamer/FreeStreamer/file_output.h
generated
Normal 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
|
||||
405
Pods/FreeStreamer/FreeStreamer/FreeStreamer/file_stream.cpp
generated
Normal 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
|
||||
64
Pods/FreeStreamer/FreeStreamer/FreeStreamer/file_stream.h
generated
Normal 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
|
||||
908
Pods/FreeStreamer/FreeStreamer/FreeStreamer/http_stream.cpp
generated
Normal 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
|
||||