├── .gitignore
├── .swift-version
├── Demo
├── Demo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ └── contents.xcworkspacedata
├── Demo.xcworkspace
│ └── contents.xcworkspacedata
├── Demo
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── ViewController.swift
├── Podfile
└── Podfile.lock
├── LICENSE
├── LemonDeer.podspec
├── LemonDeer
├── Assets
│ └── .gitkeep
└── Classes
│ ├── .gitkeep
│ ├── LemonDeer.swift
│ ├── M3u8Parser.swift
│ ├── M3u8Playlist.swift
│ ├── M3u8TsSegmentModel.swift
│ ├── SegmentDownloader.swift
│ ├── VideoDownloader.swift
│ └── VideoDownloaderHelper.swift
├── Package.swift
├── README.md
├── Resources
└── LemonDeer-logo.png
├── Sources
├── DownloadManager.swift
├── LemonDeer.swift
├── M3u8Parser.swift
├── M3u8Playlist.swift
├── M3u8TsSegmentModel.swift
├── SegmentDownloader.swift
├── VideoDownloader.swift
└── VideoDownloaderHelper.swift
└── _Pods.xcodeproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Carthage ###
2 | Carthage/Build
3 |
4 | ### CocoaPods ###
5 | Pods/
6 | examples/Pods
7 | examples/**/Pods
8 | coverage/
9 | .coveralls.yml
10 |
11 | ### macOS ###
12 | *.DS_Store
13 | .AppleDouble
14 | .LSOverride
15 |
16 | # Icon must end with two \r
17 | Icon
18 |
19 | # Thumbnails
20 | ._*
21 |
22 | # Files that might appear in the root of a volume
23 | .DocumentRevisions-V100
24 | .fseventsd
25 | .Spotlight-V100
26 | .TemporaryItems
27 | .Trashes
28 | .VolumeIcon.icns
29 | .com.apple.timemachine.donotpresent
30 |
31 | # Directories potentially created on remote AFP share
32 | .AppleDB
33 | .AppleDesktop
34 | Network Trash Folder
35 | Temporary Items
36 | .apdisk
37 |
38 | ## Build generated
39 | build/
40 | DerivedData/
41 |
42 | ## Various settings
43 | *.pbxuser
44 | !default.pbxuser
45 | *.mode1v3
46 | !default.mode1v3
47 | *.mode2v3
48 | !default.mode2v3
49 | *.perspectivev3
50 | !default.perspectivev3
51 | xcuserdata/
52 |
53 | ## Other
54 | *.moved-aside
55 | *.xccheckout
56 | *.xcscmblueprint
57 |
58 | ## Obj-C/Swift specific
59 | *.hmap
60 | *.ipa
61 | *.dSYM.zip
62 | *.dSYM
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 3.0
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 42A90C25CFF382F2513DCF3F /* Pods_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 936E65AD88DA0F1D1A45085C /* Pods_Demo.framework */; };
11 | EA263DC11EFCC4EE00649FB3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA263DC01EFCC4EE00649FB3 /* AppDelegate.swift */; };
12 | EA263DC31EFCC4EE00649FB3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA263DC21EFCC4EE00649FB3 /* ViewController.swift */; };
13 | EA263DC61EFCC4EE00649FB3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EA263DC41EFCC4EE00649FB3 /* Main.storyboard */; };
14 | EA263DC81EFCC4EE00649FB3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EA263DC71EFCC4EE00649FB3 /* Assets.xcassets */; };
15 | EA263DCB1EFCC4EE00649FB3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EA263DC91EFCC4EE00649FB3 /* LaunchScreen.storyboard */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | 6AA36D72764771C9F1196AA0 /* Pods-Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.release.xcconfig"; path = "Pods/Target Support Files/Pods-Demo/Pods-Demo.release.xcconfig"; sourceTree = ""; };
20 | 936E65AD88DA0F1D1A45085C /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
21 | EA263DBD1EFCC4EE00649FB3 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
22 | EA263DC01EFCC4EE00649FB3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
23 | EA263DC21EFCC4EE00649FB3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
24 | EA263DC51EFCC4EE00649FB3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
25 | EA263DC71EFCC4EE00649FB3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
26 | EA263DCA1EFCC4EE00649FB3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
27 | EA263DCC1EFCC4EE00649FB3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
28 | F243F064008530A8580843D2 /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; };
29 | /* End PBXFileReference section */
30 |
31 | /* Begin PBXFrameworksBuildPhase section */
32 | EA263DBA1EFCC4EE00649FB3 /* Frameworks */ = {
33 | isa = PBXFrameworksBuildPhase;
34 | buildActionMask = 2147483647;
35 | files = (
36 | 42A90C25CFF382F2513DCF3F /* Pods_Demo.framework in Frameworks */,
37 | );
38 | runOnlyForDeploymentPostprocessing = 0;
39 | };
40 | /* End PBXFrameworksBuildPhase section */
41 |
42 | /* Begin PBXGroup section */
43 | 736DF457A3BAC39B2371ED43 /* Frameworks */ = {
44 | isa = PBXGroup;
45 | children = (
46 | 936E65AD88DA0F1D1A45085C /* Pods_Demo.framework */,
47 | );
48 | name = Frameworks;
49 | sourceTree = "";
50 | };
51 | D80BAC0326A1130D510F78A7 /* Pods */ = {
52 | isa = PBXGroup;
53 | children = (
54 | F243F064008530A8580843D2 /* Pods-Demo.debug.xcconfig */,
55 | 6AA36D72764771C9F1196AA0 /* Pods-Demo.release.xcconfig */,
56 | );
57 | name = Pods;
58 | sourceTree = "";
59 | };
60 | EA263DB41EFCC4EE00649FB3 = {
61 | isa = PBXGroup;
62 | children = (
63 | EA263DBF1EFCC4EE00649FB3 /* Demo */,
64 | EA263DBE1EFCC4EE00649FB3 /* Products */,
65 | D80BAC0326A1130D510F78A7 /* Pods */,
66 | 736DF457A3BAC39B2371ED43 /* Frameworks */,
67 | );
68 | sourceTree = "";
69 | };
70 | EA263DBE1EFCC4EE00649FB3 /* Products */ = {
71 | isa = PBXGroup;
72 | children = (
73 | EA263DBD1EFCC4EE00649FB3 /* Demo.app */,
74 | );
75 | name = Products;
76 | sourceTree = "";
77 | };
78 | EA263DBF1EFCC4EE00649FB3 /* Demo */ = {
79 | isa = PBXGroup;
80 | children = (
81 | EA263DC01EFCC4EE00649FB3 /* AppDelegate.swift */,
82 | EA263DC21EFCC4EE00649FB3 /* ViewController.swift */,
83 | EA263DC41EFCC4EE00649FB3 /* Main.storyboard */,
84 | EA263DC71EFCC4EE00649FB3 /* Assets.xcassets */,
85 | EA263DC91EFCC4EE00649FB3 /* LaunchScreen.storyboard */,
86 | EA263DCC1EFCC4EE00649FB3 /* Info.plist */,
87 | );
88 | path = Demo;
89 | sourceTree = "";
90 | };
91 | /* End PBXGroup section */
92 |
93 | /* Begin PBXNativeTarget section */
94 | EA263DBC1EFCC4EE00649FB3 /* Demo */ = {
95 | isa = PBXNativeTarget;
96 | buildConfigurationList = EA263DCF1EFCC4EE00649FB3 /* Build configuration list for PBXNativeTarget "Demo" */;
97 | buildPhases = (
98 | CBA9225132157C95F9F3B6AB /* [CP] Check Pods Manifest.lock */,
99 | EA263DB91EFCC4EE00649FB3 /* Sources */,
100 | EA263DBA1EFCC4EE00649FB3 /* Frameworks */,
101 | EA263DBB1EFCC4EE00649FB3 /* Resources */,
102 | 371153AB537F15A53EB79762 /* [CP] Embed Pods Frameworks */,
103 | 3D5F4C8AB2CC730FF9BB2D64 /* [CP] Copy Pods Resources */,
104 | );
105 | buildRules = (
106 | );
107 | dependencies = (
108 | );
109 | name = Demo;
110 | productName = Demo;
111 | productReference = EA263DBD1EFCC4EE00649FB3 /* Demo.app */;
112 | productType = "com.apple.product-type.application";
113 | };
114 | /* End PBXNativeTarget section */
115 |
116 | /* Begin PBXProject section */
117 | EA263DB51EFCC4EE00649FB3 /* Project object */ = {
118 | isa = PBXProject;
119 | attributes = {
120 | LastSwiftUpdateCheck = 0830;
121 | LastUpgradeCheck = 0830;
122 | ORGANIZATIONNAME = Ziyideas;
123 | TargetAttributes = {
124 | EA263DBC1EFCC4EE00649FB3 = {
125 | CreatedOnToolsVersion = 8.3.3;
126 | DevelopmentTeam = J8DC9PK5S4;
127 | ProvisioningStyle = Automatic;
128 | };
129 | };
130 | };
131 | buildConfigurationList = EA263DB81EFCC4EE00649FB3 /* Build configuration list for PBXProject "Demo" */;
132 | compatibilityVersion = "Xcode 3.2";
133 | developmentRegion = English;
134 | hasScannedForEncodings = 0;
135 | knownRegions = (
136 | en,
137 | Base,
138 | );
139 | mainGroup = EA263DB41EFCC4EE00649FB3;
140 | productRefGroup = EA263DBE1EFCC4EE00649FB3 /* Products */;
141 | projectDirPath = "";
142 | projectRoot = "";
143 | targets = (
144 | EA263DBC1EFCC4EE00649FB3 /* Demo */,
145 | );
146 | };
147 | /* End PBXProject section */
148 |
149 | /* Begin PBXResourcesBuildPhase section */
150 | EA263DBB1EFCC4EE00649FB3 /* Resources */ = {
151 | isa = PBXResourcesBuildPhase;
152 | buildActionMask = 2147483647;
153 | files = (
154 | EA263DCB1EFCC4EE00649FB3 /* LaunchScreen.storyboard in Resources */,
155 | EA263DC81EFCC4EE00649FB3 /* Assets.xcassets in Resources */,
156 | EA263DC61EFCC4EE00649FB3 /* Main.storyboard in Resources */,
157 | );
158 | runOnlyForDeploymentPostprocessing = 0;
159 | };
160 | /* End PBXResourcesBuildPhase section */
161 |
162 | /* Begin PBXShellScriptBuildPhase section */
163 | 371153AB537F15A53EB79762 /* [CP] Embed Pods Frameworks */ = {
164 | isa = PBXShellScriptBuildPhase;
165 | buildActionMask = 2147483647;
166 | files = (
167 | );
168 | inputPaths = (
169 | "${SRCROOT}/Pods/Target Support Files/Pods-Demo/Pods-Demo-frameworks.sh",
170 | "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework",
171 | "${BUILT_PRODUCTS_DIR}/GCDWebServer/GCDWebServer.framework",
172 | "${BUILT_PRODUCTS_DIR}/LemonDeer/LemonDeer.framework",
173 | );
174 | name = "[CP] Embed Pods Frameworks";
175 | outputPaths = (
176 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework",
177 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GCDWebServer.framework",
178 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LemonDeer.framework",
179 | );
180 | runOnlyForDeploymentPostprocessing = 0;
181 | shellPath = /bin/sh;
182 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Demo/Pods-Demo-frameworks.sh\"\n";
183 | showEnvVarsInLog = 0;
184 | };
185 | 3D5F4C8AB2CC730FF9BB2D64 /* [CP] Copy Pods Resources */ = {
186 | isa = PBXShellScriptBuildPhase;
187 | buildActionMask = 2147483647;
188 | files = (
189 | );
190 | inputPaths = (
191 | );
192 | name = "[CP] Copy Pods Resources";
193 | outputPaths = (
194 | );
195 | runOnlyForDeploymentPostprocessing = 0;
196 | shellPath = /bin/sh;
197 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Demo/Pods-Demo-resources.sh\"\n";
198 | showEnvVarsInLog = 0;
199 | };
200 | CBA9225132157C95F9F3B6AB /* [CP] Check Pods Manifest.lock */ = {
201 | isa = PBXShellScriptBuildPhase;
202 | buildActionMask = 2147483647;
203 | files = (
204 | );
205 | inputPaths = (
206 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
207 | "${PODS_ROOT}/Manifest.lock",
208 | );
209 | name = "[CP] Check Pods Manifest.lock";
210 | outputPaths = (
211 | "$(DERIVED_FILE_DIR)/Pods-Demo-checkManifestLockResult.txt",
212 | );
213 | runOnlyForDeploymentPostprocessing = 0;
214 | shellPath = /bin/sh;
215 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
216 | showEnvVarsInLog = 0;
217 | };
218 | /* End PBXShellScriptBuildPhase section */
219 |
220 | /* Begin PBXSourcesBuildPhase section */
221 | EA263DB91EFCC4EE00649FB3 /* Sources */ = {
222 | isa = PBXSourcesBuildPhase;
223 | buildActionMask = 2147483647;
224 | files = (
225 | EA263DC31EFCC4EE00649FB3 /* ViewController.swift in Sources */,
226 | EA263DC11EFCC4EE00649FB3 /* AppDelegate.swift in Sources */,
227 | );
228 | runOnlyForDeploymentPostprocessing = 0;
229 | };
230 | /* End PBXSourcesBuildPhase section */
231 |
232 | /* Begin PBXVariantGroup section */
233 | EA263DC41EFCC4EE00649FB3 /* Main.storyboard */ = {
234 | isa = PBXVariantGroup;
235 | children = (
236 | EA263DC51EFCC4EE00649FB3 /* Base */,
237 | );
238 | name = Main.storyboard;
239 | sourceTree = "";
240 | };
241 | EA263DC91EFCC4EE00649FB3 /* LaunchScreen.storyboard */ = {
242 | isa = PBXVariantGroup;
243 | children = (
244 | EA263DCA1EFCC4EE00649FB3 /* Base */,
245 | );
246 | name = LaunchScreen.storyboard;
247 | sourceTree = "";
248 | };
249 | /* End PBXVariantGroup section */
250 |
251 | /* Begin XCBuildConfiguration section */
252 | EA263DCD1EFCC4EE00649FB3 /* Debug */ = {
253 | isa = XCBuildConfiguration;
254 | buildSettings = {
255 | ALWAYS_SEARCH_USER_PATHS = NO;
256 | CLANG_ANALYZER_NONNULL = YES;
257 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
258 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
259 | CLANG_CXX_LIBRARY = "libc++";
260 | CLANG_ENABLE_MODULES = YES;
261 | CLANG_ENABLE_OBJC_ARC = YES;
262 | CLANG_WARN_BOOL_CONVERSION = YES;
263 | CLANG_WARN_CONSTANT_CONVERSION = YES;
264 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
265 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
266 | CLANG_WARN_EMPTY_BODY = YES;
267 | CLANG_WARN_ENUM_CONVERSION = YES;
268 | CLANG_WARN_INFINITE_RECURSION = YES;
269 | CLANG_WARN_INT_CONVERSION = YES;
270 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
271 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
272 | CLANG_WARN_UNREACHABLE_CODE = YES;
273 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
274 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
275 | COPY_PHASE_STRIP = NO;
276 | DEBUG_INFORMATION_FORMAT = dwarf;
277 | ENABLE_STRICT_OBJC_MSGSEND = YES;
278 | ENABLE_TESTABILITY = YES;
279 | GCC_C_LANGUAGE_STANDARD = gnu99;
280 | GCC_DYNAMIC_NO_PIC = NO;
281 | GCC_NO_COMMON_BLOCKS = YES;
282 | GCC_OPTIMIZATION_LEVEL = 0;
283 | GCC_PREPROCESSOR_DEFINITIONS = (
284 | "DEBUG=1",
285 | "$(inherited)",
286 | );
287 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
288 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
289 | GCC_WARN_UNDECLARED_SELECTOR = YES;
290 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
291 | GCC_WARN_UNUSED_FUNCTION = YES;
292 | GCC_WARN_UNUSED_VARIABLE = YES;
293 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
294 | MTL_ENABLE_DEBUG_INFO = YES;
295 | ONLY_ACTIVE_ARCH = YES;
296 | SDKROOT = iphoneos;
297 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
298 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
299 | TARGETED_DEVICE_FAMILY = "1,2";
300 | };
301 | name = Debug;
302 | };
303 | EA263DCE1EFCC4EE00649FB3 /* Release */ = {
304 | isa = XCBuildConfiguration;
305 | buildSettings = {
306 | ALWAYS_SEARCH_USER_PATHS = NO;
307 | CLANG_ANALYZER_NONNULL = YES;
308 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
310 | CLANG_CXX_LIBRARY = "libc++";
311 | CLANG_ENABLE_MODULES = YES;
312 | CLANG_ENABLE_OBJC_ARC = YES;
313 | CLANG_WARN_BOOL_CONVERSION = YES;
314 | CLANG_WARN_CONSTANT_CONVERSION = YES;
315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
317 | CLANG_WARN_EMPTY_BODY = YES;
318 | CLANG_WARN_ENUM_CONVERSION = YES;
319 | CLANG_WARN_INFINITE_RECURSION = YES;
320 | CLANG_WARN_INT_CONVERSION = YES;
321 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
322 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
323 | CLANG_WARN_UNREACHABLE_CODE = YES;
324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
325 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
326 | COPY_PHASE_STRIP = NO;
327 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
328 | ENABLE_NS_ASSERTIONS = NO;
329 | ENABLE_STRICT_OBJC_MSGSEND = YES;
330 | GCC_C_LANGUAGE_STANDARD = gnu99;
331 | GCC_NO_COMMON_BLOCKS = YES;
332 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
333 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
334 | GCC_WARN_UNDECLARED_SELECTOR = YES;
335 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
336 | GCC_WARN_UNUSED_FUNCTION = YES;
337 | GCC_WARN_UNUSED_VARIABLE = YES;
338 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
339 | MTL_ENABLE_DEBUG_INFO = NO;
340 | SDKROOT = iphoneos;
341 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
342 | TARGETED_DEVICE_FAMILY = "1,2";
343 | VALIDATE_PRODUCT = YES;
344 | };
345 | name = Release;
346 | };
347 | EA263DD01EFCC4EE00649FB3 /* Debug */ = {
348 | isa = XCBuildConfiguration;
349 | baseConfigurationReference = F243F064008530A8580843D2 /* Pods-Demo.debug.xcconfig */;
350 | buildSettings = {
351 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
352 | DEVELOPMENT_TEAM = J8DC9PK5S4;
353 | INFOPLIST_FILE = Demo/Info.plist;
354 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
355 | PRODUCT_BUNDLE_IDENTIFIER = com.Ziyideas.WindmillComic.Demo;
356 | PRODUCT_NAME = "$(TARGET_NAME)";
357 | SWIFT_VERSION = 3.0;
358 | };
359 | name = Debug;
360 | };
361 | EA263DD11EFCC4EE00649FB3 /* Release */ = {
362 | isa = XCBuildConfiguration;
363 | baseConfigurationReference = 6AA36D72764771C9F1196AA0 /* Pods-Demo.release.xcconfig */;
364 | buildSettings = {
365 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
366 | DEVELOPMENT_TEAM = J8DC9PK5S4;
367 | INFOPLIST_FILE = Demo/Info.plist;
368 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
369 | PRODUCT_BUNDLE_IDENTIFIER = com.Ziyideas.WindmillComic.Demo;
370 | PRODUCT_NAME = "$(TARGET_NAME)";
371 | SWIFT_VERSION = 3.0;
372 | };
373 | name = Release;
374 | };
375 | /* End XCBuildConfiguration section */
376 |
377 | /* Begin XCConfigurationList section */
378 | EA263DB81EFCC4EE00649FB3 /* Build configuration list for PBXProject "Demo" */ = {
379 | isa = XCConfigurationList;
380 | buildConfigurations = (
381 | EA263DCD1EFCC4EE00649FB3 /* Debug */,
382 | EA263DCE1EFCC4EE00649FB3 /* Release */,
383 | );
384 | defaultConfigurationIsVisible = 0;
385 | defaultConfigurationName = Release;
386 | };
387 | EA263DCF1EFCC4EE00649FB3 /* Build configuration list for PBXNativeTarget "Demo" */ = {
388 | isa = XCConfigurationList;
389 | buildConfigurations = (
390 | EA263DD01EFCC4EE00649FB3 /* Debug */,
391 | EA263DD11EFCC4EE00649FB3 /* Release */,
392 | );
393 | defaultConfigurationIsVisible = 0;
394 | };
395 | /* End XCConfigurationList section */
396 | };
397 | rootObject = EA263DB51EFCC4EE00649FB3 /* Project object */;
398 | }
399 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Demo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Demo/Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demo
4 | //
5 | // Created by Ziyi Zhang on 23/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "ipad",
35 | "size" : "29x29",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "29x29",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "40x40",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "40x40",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "76x76",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "76x76",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Demo/Demo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Demo/Demo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
42 |
51 |
60 |
69 |
70 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/Demo/Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | NSAppTransportSecurity
45 |
46 | NSAllowsArbitraryLoads
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Demo/Demo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Demo
4 | //
5 | // Created by Ziyi Zhang on 23/06/2017.
6 | // Copyright © 2017 hippo_san. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | import LemonDeer
13 | import GCDWebServer
14 |
15 | class ViewController: UIViewController {
16 | @IBOutlet var progressLabel: UILabel!
17 | @IBOutlet var downloadButton: UIButton!
18 | @IBOutlet var playerView: UIView!
19 |
20 | fileprivate var isDownloading = false
21 | fileprivate var duringDownloadingProcess = false
22 |
23 | private let lemonDeer = LemonDeer()
24 | private var server: GCDWebServer! = nil
25 | private var player = AVPlayer()
26 | private var playerLayer = AVPlayerLayer()
27 |
28 | @IBAction func download(_ sender: Any) {
29 | if !isDownloading {
30 | DispatchQueue.main.async {
31 | self.downloadButton.setTitle("Pause", for: .normal)
32 | }
33 |
34 | isDownloading = true
35 |
36 | if duringDownloadingProcess {
37 | lemonDeer.downloader.resumeDownloadSegment()
38 | } else {
39 | let url = "http://pl-ali.youku.com/playlist/m3u8?ts=1497413452&keyframe=1&vid=704675076&type=hd2&sid=0497413452394200e1f61&token=8269&oip=1696929637&did=2739b348d6020958407ddebff48b76bd&ctype=20&ev=1&ep=yZa8BLwhkewm%2BJYwNpWin%2BP9q1Xl%2FcCHzQ80y23Oig6QzYfmciUxpx%2F65Yk1CRBd"
40 |
41 | lemonDeer.directoryName = "Demo"
42 | lemonDeer.m3u8URL = url
43 | lemonDeer.delegate = self
44 | lemonDeer.parse()
45 | }
46 | } else {
47 | DispatchQueue.main.async {
48 | self.downloadButton.setTitle("Download", for: .normal)
49 | }
50 |
51 | isDownloading = false
52 | duringDownloadingProcess = true
53 |
54 | lemonDeer.downloader.pauseDownloadSegment()
55 | }
56 | }
57 | @IBAction func playOnlineVideo(_ sender: Any) {
58 | configurePlayer(with: "http://pl-ali.youku.com/playlist/m3u8?ts=1497413452&keyframe=1&vid=704675076&type=hd2&sid=0497413452394200e1f61&token=8269&oip=1696929637&did=2739b348d6020958407ddebff48b76bd&ctype=20&ev=1&ep=yZa8BLwhkewm%2BJYwNpWin%2BP9q1Xl%2FcCHzQ80y23Oig6QzYfmciUxpx%2F65Yk1CRBd")
59 | }
60 |
61 | @IBAction func playLocalVideo(_ sender: Any) {
62 | server = GCDWebDAVServer(uploadDirectory: getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent("Demo").path)
63 | server.start()
64 |
65 | configurePlayer(with: "http://127.0.0.1:8080/Demo.m3u8")
66 | }
67 |
68 | @IBAction func deleteDownloadedContents(_ sender: Any) {
69 | lemonDeer.downloader.deleteAllDownloadedContents()
70 |
71 | progressLabel.text = "0 %"
72 | }
73 |
74 | @IBAction func deleteContentWithName(_ sender: Any) {
75 | let alert = UIAlertController(title: "Delete Content", message: "Input the name of directory you want to delete.", preferredStyle: .alert)
76 | alert.addTextField()
77 |
78 | let confirmAction = UIAlertAction(title: "OK", style: .default) { [weak alert] _ in
79 | self.lemonDeer.downloader.deleteDownloadedContents(with: (alert?.textFields?[0].text)!)
80 |
81 | self.progressLabel.text = " "
82 | }
83 |
84 | let cancelAction = UIAlertAction(title: "Cancel", style: .default) { [weak alert] _ in
85 | alert?.dismiss(animated: true)
86 | }
87 |
88 | alert.addAction(confirmAction)
89 | alert.addAction(cancelAction)
90 |
91 | present(alert, animated: true)
92 | }
93 |
94 | private func configurePlayer(with url: String) {
95 | player.pause()
96 | playerLayer.removeFromSuperlayer()
97 |
98 | player = AVPlayer(url: URL(string: url)!)
99 | playerLayer = AVPlayerLayer(player: player)
100 | playerLayer.frame = CGRect(x: 0, y: 0, width: playerView.bounds.width, height: playerView.bounds.height)
101 | playerView.layer.addSublayer(playerLayer)
102 |
103 | player.play()
104 | }
105 | }
106 |
107 | extension ViewController: LemonDeerDelegate {
108 | func videoDownloadSucceeded() {
109 | print("Video download succeeded.")
110 |
111 | isDownloading = false
112 | duringDownloadingProcess = false
113 |
114 | DispatchQueue.main.async {
115 | self.downloadButton.setTitle("Finished", for: .normal)
116 | }
117 |
118 | downloadButton.isUserInteractionEnabled = false
119 | }
120 |
121 | func videoDownloadFailed() {
122 | print("Video download failed.")
123 | }
124 |
125 | func update(_ progress: Float, with directoryName: String) {
126 | progressLabel.text = "\(progress * 100) %"
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Demo/Podfile:
--------------------------------------------------------------------------------
1 | use_frameworks!
2 |
3 | target 'Demo' do
4 | pod 'LemonDeer', :path => '../'
5 |
6 | pod 'CocoaLumberjack/Swift', "~> 3.2.0"
7 | pod "GCDWebServer/WebDAV", "~> 3.0"
8 | end
9 |
--------------------------------------------------------------------------------
/Demo/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - CocoaLumberjack/Default (3.2.0)
3 | - CocoaLumberjack/Swift (3.2.0):
4 | - CocoaLumberjack/Default
5 | - GCDWebServer/Core (3.3.2)
6 | - GCDWebServer/WebDAV (3.3.2):
7 | - GCDWebServer/WebDAV/Core (= 3.3.2)
8 | - GCDWebServer/WebDAV/Core (3.3.2):
9 | - GCDWebServer/Core
10 | - LemonDeer (1.0.3)
11 |
12 | DEPENDENCIES:
13 | - CocoaLumberjack/Swift (~> 3.2.0)
14 | - GCDWebServer/WebDAV (~> 3.0)
15 | - LemonDeer (from `../`)
16 |
17 | EXTERNAL SOURCES:
18 | LemonDeer:
19 | :path: ../
20 |
21 | SPEC CHECKSUMS:
22 | CocoaLumberjack: 9b4aed7073d242f29cc2f62068d995faf67f703a
23 | GCDWebServer: 2a375ec42839a41d7187d04e5b688d32fa5c4cd5
24 | LemonDeer: c871306e11cca5cc53105fd30d94b1e8460563f1
25 |
26 | PODFILE CHECKSUM: 68812568e014cf7fdfd3ccf77acbeb7bda1ea0cf
27 |
28 | COCOAPODS: 1.3.0.beta.2
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 hippo_san
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/LemonDeer.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'LemonDeer'
3 | s.version = '1.0.3'
4 | s.summary = 'Make m3u8 parse and download as a breeze.'
5 |
6 | s.description = <<-DESC
7 | LemonDeer is an iOS framewrok that parses m3u8 file and downloads videos easy as breeze. It is written purely in Swift, along with several useful customizable methods.
8 | DESC
9 |
10 | s.homepage = 'https://github.com/hipposan/LemonDeer'
11 | s.screenshots = 'https://raw.githubusercontent.com/hipposan/LemonDeer/master/Resources/LemonDeer-logo.png'
12 | s.license = { :type => 'MIT', :file => 'LICENSE' }
13 | s.author = { 'hippo_san' => 'zzy0600@gmail.com' }
14 | s.source = { :git => 'https://github.com/hipposan/LemonDeer.git', :tag => s.version.to_s }
15 | s.social_media_url = 'https://github.com/hipposan'
16 |
17 | s.ios.deployment_target = '9.0'
18 |
19 | s.source_files = 'LemonDeer/Classes/**/*'
20 | end
21 |
--------------------------------------------------------------------------------
/LemonDeer/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hipposan/LemonDeer/bfb0d9a6225ca9eaf0cec525033ddcb45083325d/LemonDeer/Assets/.gitkeep
--------------------------------------------------------------------------------
/LemonDeer/Classes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hipposan/LemonDeer/bfb0d9a6225ca9eaf0cec525033ddcb45083325d/LemonDeer/Classes/.gitkeep
--------------------------------------------------------------------------------
/LemonDeer/Classes/LemonDeer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LemonDeer.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol LemonDeerDelegate: class {
12 | func videoDownloadSucceeded()
13 | func videoDownloadFailed()
14 |
15 | func update(_ progress: Float, with directoryName: String)
16 | }
17 |
18 | open class LemonDeer {
19 | public let downloader = VideoDownloader()
20 | public var progress: Float = 0.0
21 | public var directoryName: String = "" {
22 | didSet {
23 | m3u8Parser.identifier = directoryName
24 | }
25 | }
26 | public var m3u8URL = ""
27 |
28 | private let m3u8Parser = M3u8Parser()
29 |
30 | public weak var delegate: LemonDeerDelegate?
31 |
32 | public init() {
33 |
34 | }
35 |
36 | open func parse() {
37 | downloader.delegate = self
38 | m3u8Parser.delegate = self
39 | m3u8Parser.parse(with: m3u8URL)
40 | }
41 | }
42 |
43 | extension LemonDeer: M3u8ParserDelegate {
44 | func parseM3u8Succeeded(by parser: M3u8Parser) {
45 | downloader.tsPlaylist = parser.tsPlaylist
46 | downloader.m3u8Data = parser.m3u8Data
47 | downloader.startDownload()
48 | }
49 |
50 | func parseM3u8Failed(by parser: M3u8Parser) {
51 | print("Parse m3u8 file failed.")
52 | }
53 | }
54 |
55 | extension LemonDeer: VideoDownloaderDelegate {
56 | func videoDownloadSucceeded(by downloader: VideoDownloader) {
57 | delegate?.videoDownloadSucceeded()
58 | }
59 |
60 | func videoDownloadFailed(by downloader: VideoDownloader) {
61 | delegate?.videoDownloadFailed()
62 | }
63 |
64 | func update(_ progress: Float) {
65 | self.progress = progress
66 |
67 | delegate?.update(progress, with: directoryName)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/LemonDeer/Classes/M3u8Parser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // m3u8Handler.swift
3 | // WindmillComic
4 | //
5 | // Created by hippo_san on 08/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol M3u8ParserDelegate: class {
12 | func parseM3u8Succeeded(by parser: M3u8Parser)
13 | func parseM3u8Failed(by parser: M3u8Parser)
14 | }
15 |
16 | open class M3u8Parser {
17 | weak var delegate: M3u8ParserDelegate?
18 |
19 | var m3u8Data: String = ""
20 | var tsSegmentArray = [M3u8TsSegmentModel]()
21 | var tsPlaylist = M3u8Playlist()
22 | var identifier = ""
23 |
24 | /**
25 | To parse m3u8 file with a provided URL.
26 |
27 | - parameter url: A string of URL you want to parse.
28 | */
29 | open func parse(with url: String) {
30 | guard let m3u8ParserDelegate = delegate else {
31 | print("M3u8ParserDelegate not set.")
32 | return
33 | }
34 |
35 | if !(url.hasPrefix("http://") || url.hasPrefix("https://")) {
36 | print("Invalid URL.")
37 | m3u8ParserDelegate.parseM3u8Failed(by: self)
38 | return
39 | }
40 |
41 | DispatchQueue.global(qos: .background).async {
42 | do {
43 | let m3u8Content = try String(contentsOf: URL(string: url)!, encoding: .utf8)
44 |
45 | if m3u8Content == "" {
46 | print("Empty m3u8 content.")
47 | m3u8ParserDelegate.parseM3u8Failed(by: self)
48 | return
49 | } else {
50 | guard (m3u8Content.range(of: "#EXTINF:") != nil) else {
51 | print("No EXTINF info.")
52 | m3u8ParserDelegate.parseM3u8Failed(by: self)
53 | return
54 | }
55 |
56 | self.m3u8Data = m3u8Content
57 | if self.tsSegmentArray.count > 0 { self.tsSegmentArray.removeAll() }
58 |
59 | let segmentRange = m3u8Content.range(of: "#EXTINF:")!
60 | let segmentsString = String(m3u8Content.characters.suffix(from: segmentRange.lowerBound)).components(separatedBy: "#EXT-X-ENDLIST")
61 | var segmentArray = segmentsString[0].components(separatedBy: "\n")
62 | segmentArray = segmentArray.filter { !$0.contains("#EXT-X-DISCONTINUITY") }
63 |
64 | while (segmentArray.count > 2) {
65 | var segmentModel = M3u8TsSegmentModel()
66 |
67 | let segmentDurationPart = segmentArray[0].components(separatedBy: ":")[1]
68 | var segmentDuration: Float = 0.0
69 |
70 | if segmentDurationPart.contains(",") {
71 | segmentDuration = Float(segmentDurationPart.components(separatedBy: ",")[0])!
72 | } else {
73 | segmentDuration = Float(segmentDurationPart)!
74 | }
75 |
76 | let segmentURL = segmentArray[1]
77 | segmentModel.duration = segmentDuration
78 | segmentModel.locationURL = segmentURL
79 |
80 | self.tsSegmentArray.append(segmentModel)
81 |
82 | segmentArray.remove(at: 0)
83 | segmentArray.remove(at: 0)
84 | }
85 |
86 | self.tsPlaylist.initSegment(with: self.tsSegmentArray)
87 | self.tsPlaylist.identifier = self.identifier
88 |
89 | m3u8ParserDelegate.parseM3u8Succeeded(by: self)
90 | }
91 | } catch let error {
92 | print(error.localizedDescription)
93 | print("Read m3u8 file content error.")
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/LemonDeer/Classes/M3u8Playlist.swift:
--------------------------------------------------------------------------------
1 | //
2 | // M3u8Playlist.swift
3 | // WindmillComic
4 | //
5 | // Created by hippo_san on 08/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class M3u8Playlist {
12 | var tsSegmentArray = [M3u8TsSegmentModel]()
13 | var length = 0
14 | var identifier = ""
15 |
16 | func initSegment(with array: [M3u8TsSegmentModel]) {
17 | tsSegmentArray = array
18 | length = array.count
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/LemonDeer/Classes/M3u8TsSegmentModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // M3u8TsSegmentModel.swift
3 | // WindmillComic
4 | //
5 | // Created by hippo_san on 08/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct M3u8TsSegmentModel {
12 | var duration: Float = 0.0
13 | var locationURL = ""
14 | var index: Int = 0
15 | }
16 |
--------------------------------------------------------------------------------
/LemonDeer/Classes/SegmentDownloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SegmentDownloader.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol SegmentDownloaderDelegate {
12 | func segmentDownloadSucceeded(with downloader: SegmentDownloader)
13 | func segmentDownloadFailed(with downloader: SegmentDownloader)
14 | }
15 |
16 | class SegmentDownloader: NSObject {
17 | var fileName: String
18 | var filePath: String
19 | var downloadURL: String
20 | var duration: Float
21 | var index: Int
22 |
23 | lazy var downloadSession: URLSession = {
24 | let configuration = URLSessionConfiguration.default
25 | let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
26 |
27 | return session
28 | }()
29 |
30 | var downloadTask: URLSessionDownloadTask?
31 | var isDownloading = false
32 | var finishedDownload = false
33 |
34 | var delegate: SegmentDownloaderDelegate?
35 |
36 | init(with url: String, filePath: String, fileName: String, duration: Float, index: Int) {
37 | downloadURL = url
38 | self.filePath = filePath
39 | self.fileName = fileName
40 | self.duration = duration
41 | self.index = index
42 | }
43 |
44 | func startDownload() {
45 | if checkIfIsDownloaded() {
46 | finishedDownload = true
47 |
48 | delegate?.segmentDownloadSucceeded(with: self)
49 | } else {
50 | let url = downloadURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
51 |
52 | guard let taskURL = URL(string: url) else { return }
53 |
54 | downloadTask = downloadSession.downloadTask(with: taskURL)
55 | downloadTask?.resume()
56 | isDownloading = true
57 | }
58 | }
59 |
60 | func cancelDownload() {
61 | downloadTask?.cancel()
62 | isDownloading = false
63 | }
64 |
65 | func pauseDownload() {
66 | if isDownloading {
67 | downloadTask?.suspend()
68 |
69 | isDownloading = false
70 | }
71 | }
72 |
73 | func resumeDownload() {
74 | downloadTask?.resume()
75 | isDownloading = true
76 | }
77 |
78 | func checkIfIsDownloaded() -> Bool {
79 | let filePath = generateFilePath().path
80 |
81 | if FileManager.default.fileExists(atPath: filePath) {
82 | return true
83 | } else {
84 | return false
85 | }
86 | }
87 |
88 | func generateFilePath() -> URL {
89 | return getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(filePath).appendingPathComponent(fileName)
90 | }
91 | }
92 |
93 | extension SegmentDownloader: URLSessionDownloadDelegate {
94 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
95 | let destinationURL = generateFilePath()
96 |
97 | finishedDownload = true
98 | isDownloading = false
99 |
100 | if FileManager.default.fileExists(atPath: destinationURL.path) {
101 | return
102 | } else {
103 | do {
104 | try FileManager.default.moveItem(at: location, to: destinationURL)
105 | delegate?.segmentDownloadSucceeded(with: self)
106 | } catch let error as NSError {
107 | print(error.localizedDescription)
108 | }
109 | }
110 | }
111 |
112 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
113 | if error != nil {
114 | finishedDownload = false
115 | isDownloading = false
116 |
117 | delegate?.segmentDownloadFailed(with: self)
118 | }
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/LemonDeer/Classes/VideoDownloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoDownloader.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum Status {
12 | case started
13 | case paused
14 | case canceled
15 | case finished
16 | }
17 |
18 | protocol VideoDownloaderDelegate {
19 | func videoDownloadSucceeded(by downloader: VideoDownloader)
20 | func videoDownloadFailed(by downloader: VideoDownloader)
21 |
22 | func update(_ progress: Float)
23 | }
24 |
25 | open class VideoDownloader {
26 | public var downloadStatus: Status = .paused
27 |
28 | var m3u8Data: String = ""
29 | var tsPlaylist = M3u8Playlist()
30 | var segmentDownloaders = [SegmentDownloader]()
31 | var tsFilesIndex = 0
32 | var neededDownloadTsFilesCount = 0
33 | var downloadURLs = [String]()
34 | var downloadingProgress: Float {
35 | let finishedDownloadFilesCount = segmentDownloaders.filter({ $0.finishedDownload == true }).count
36 | let fraction = Float(finishedDownloadFilesCount) / Float(neededDownloadTsFilesCount)
37 | let roundedValue = round(fraction * 100) / 100
38 |
39 | return roundedValue
40 | }
41 |
42 | fileprivate var startDownloadIndex = 2
43 |
44 | var delegate: VideoDownloaderDelegate?
45 |
46 | open func startDownload() {
47 | checkOrCreatedM3u8Directory()
48 |
49 | var newSegmentArray = [M3u8TsSegmentModel]()
50 |
51 | let notInDownloadList = tsPlaylist.tsSegmentArray.filter { !downloadURLs.contains($0.locationURL) }
52 | neededDownloadTsFilesCount = tsPlaylist.length
53 |
54 | for i in 0 ..< notInDownloadList.count {
55 | let fileName = "\(tsFilesIndex).ts"
56 |
57 | let segmentDownloader = SegmentDownloader(with: notInDownloadList[i].locationURL,
58 | filePath: tsPlaylist.identifier,
59 | fileName: fileName,
60 | duration: notInDownloadList[i].duration,
61 | index: tsFilesIndex)
62 | segmentDownloader.delegate = self
63 |
64 | segmentDownloaders.append(segmentDownloader)
65 | downloadURLs.append(notInDownloadList[i].locationURL)
66 |
67 | var segmentModel = M3u8TsSegmentModel()
68 | segmentModel.duration = segmentDownloaders[i].duration
69 | segmentModel.locationURL = segmentDownloaders[i].fileName
70 | segmentModel.index = segmentDownloaders[i].index
71 | newSegmentArray.append(segmentModel)
72 |
73 | tsPlaylist.tsSegmentArray = newSegmentArray
74 |
75 | tsFilesIndex += 1
76 | }
77 |
78 | segmentDownloaders[0].startDownload()
79 | segmentDownloaders[1].startDownload()
80 | segmentDownloaders[2].startDownload()
81 |
82 | downloadStatus = .started
83 | }
84 |
85 | func checkDownloadQueue() {
86 |
87 | }
88 |
89 | func updateLocalM3U8file() {
90 | checkOrCreatedM3u8Directory()
91 |
92 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("\(tsPlaylist.identifier).m3u8")
93 |
94 | var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n"
95 | var content = ""
96 |
97 | for i in 0 ..< tsPlaylist.tsSegmentArray.count {
98 | let segmentModel = tsPlaylist.tsSegmentArray[i]
99 |
100 | let length = "#EXTINF:\(segmentModel.duration),\n"
101 | let fileName = "http://127.0.0.1:8080/\(segmentModel.index).ts\n"
102 | content += (length + fileName)
103 | }
104 |
105 | header.append(content)
106 | header.append("#EXT-X-ENDLIST\n")
107 |
108 | let writeData: Data = header.data(using: .utf8)!
109 | try! writeData.write(to: filePath)
110 | }
111 |
112 | private func checkOrCreatedM3u8Directory() {
113 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(tsPlaylist.identifier)
114 |
115 | if !FileManager.default.fileExists(atPath: filePath.path) {
116 | try! FileManager.default.createDirectory(at: filePath, withIntermediateDirectories: true, attributes: nil)
117 | }
118 | }
119 |
120 | open func deleteAllDownloadedContents() {
121 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").path
122 |
123 | if FileManager.default.fileExists(atPath: filePath) {
124 | try! FileManager.default.removeItem(atPath: filePath)
125 | } else {
126 | print("File has already been deleted.")
127 | }
128 | }
129 |
130 | open func deleteDownloadedContents(with name: String) {
131 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(name).path
132 |
133 | if FileManager.default.fileExists(atPath: filePath) {
134 | try! FileManager.default.removeItem(atPath: filePath)
135 | } else {
136 | print("Could not find directory with name: \(name)")
137 | }
138 | }
139 |
140 | open func pauseDownloadSegment() {
141 | _ = segmentDownloaders.map { $0.pauseDownload() }
142 |
143 | downloadStatus = .paused
144 | }
145 |
146 | open func cancelDownloadSegment() {
147 | _ = segmentDownloaders.map { $0.cancelDownload() }
148 |
149 | downloadStatus = .canceled
150 | }
151 |
152 | open func resumeDownloadSegment() {
153 | _ = segmentDownloaders.map { $0.resumeDownload() }
154 |
155 | downloadStatus = .started
156 | }
157 | }
158 |
159 | extension VideoDownloader: SegmentDownloaderDelegate {
160 | func segmentDownloadSucceeded(with downloader: SegmentDownloader) {
161 | let finishedDownloadFilesCount = segmentDownloaders.filter({ $0.finishedDownload == true }).count
162 |
163 | DispatchQueue.main.async {
164 | self.delegate?.update(self.downloadingProgress)
165 | }
166 |
167 | updateLocalM3U8file()
168 |
169 | let downloadingFilesCount = segmentDownloaders.filter({ $0.isDownloading == true }).count
170 |
171 | if finishedDownloadFilesCount == neededDownloadTsFilesCount {
172 | delegate?.videoDownloadSucceeded(by: self)
173 |
174 | downloadStatus = .finished
175 | } else if startDownloadIndex == neededDownloadTsFilesCount - 1 {
176 | if segmentDownloaders[startDownloadIndex].isDownloading == true { return }
177 | }
178 | else if downloadingFilesCount < 3 || finishedDownloadFilesCount != neededDownloadTsFilesCount {
179 | if startDownloadIndex < neededDownloadTsFilesCount - 1 {
180 | startDownloadIndex += 1
181 | }
182 |
183 | segmentDownloaders[startDownloadIndex].startDownload()
184 | }
185 | }
186 |
187 | func segmentDownloadFailed(with downloader: SegmentDownloader) {
188 | delegate?.videoDownloadFailed(by: self)
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/LemonDeer/Classes/VideoDownloaderHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoDownloaderHelper.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func getDocumentsDirectory() -> URL {
12 | let paths = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask)
13 | let documentsDirectory = paths[0]
14 | return documentsDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "LemonDeer",
5 | dependencies: [
6 | .Package(url: "https://github.com/hipposan/LemonDeer.git", majorVersion: 1.0.0)
7 | ]
8 | )
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Make m3u8 parse and video download as white magic.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ___________________
31 |
32 | Features|
33 | ------------------------------- |
34 | Parse and download m3u8 files|
35 | Customize downloading progress|
36 | Pure Swift|
37 |
38 |
39 | ## Example
40 | To run the example project, clone the repo, and run `pod install` from the Example directory first.
41 |
42 |
43 | ## Requirements
44 | * Xcode 8.0+
45 | * iOS 9.0+
46 | * Swift 3.0+
47 |
48 | > **Note:**
49 | > * Your m3u8 file should include **#EXFINT** information to make parsing pass.
50 | > * Your local server's port should be **8080** to make local video play.
51 |
52 |
53 | ## Usage
54 | Define dowloading directory name:
55 |
56 | ```swift
57 | let directoryName = "Name"
58 | let lemonDeer = LemonDeer()
59 | lemonDeer.directoryName = directoryName
60 | ```
61 | ____________
62 |
63 | Parse and begin downloading m3u8 with URL:
64 |
65 | ```swift
66 | let directoryName = "Name"
67 | let lemonDeer = LemonDeer()
68 | lemonDeer.directoryName = directoryName
69 |
70 | let url = "https://urlstring.m3u8"
71 | lemonDeer.m3u8URL = url
72 | lemonDeer.parse()
73 | ```
74 | ____________
75 |
76 | Manipulate downloading process:
77 | * Pause
78 |
79 | ```swift
80 | lemonDeer.downloader.pauseDownloadSegment()
81 | ```
82 |
83 | * Resume
84 |
85 | ```swift
86 | lemonDeer.downloader.resumeDownloadSegment()
87 | ```
88 |
89 | * Cancel
90 |
91 | ```swift
92 | lemonDeer.downloader.cancelDownloadSegment()
93 | ```
94 | ____________
95 |
96 | Delete downloaded contents
97 | * Delete a specific directory
98 |
99 | ```swift
100 | lemonDeer.downloader.deleteDownloadedContents(with: ("DirectoryNameYouWantToDelete")
101 | ```
102 |
103 | * Delete all downloaded contents
104 |
105 | ```swift
106 | lemonDeer.downloader.deleteAllDownloadedContents()
107 | ```
108 | ____________
109 |
110 | Define your own after download succeeded
111 |
112 | ```swift
113 | class YourClass: LemonDeerDelegate {
114 | func videoDownloadSucceeded()
115 | }
116 | ```
117 |
118 | Define your own after download failed
119 |
120 | ```swift
121 | class YourClass: LemonDeerDelegate {
122 | func videoDownloadFailed()
123 | }
124 | ```
125 |
126 | Customize downloading progress
127 |
128 | ```swift
129 | class YourClass: LemonDeerDelegate {
130 | func update(_ progress: Float, with directoryName: String) {}
131 | }
132 | ```
133 |
134 | ## Installation
135 | ### Installation with CocoaPods
136 | LemonDeer is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile:
137 |
138 | ```ruby
139 | pod "LemonDeer"
140 | ```
141 |
142 | ### Installation with Carthage
143 | [Carthage](https://github.com/Carthage/Carthage) is a lightweight dependency manager for Swift and Objective-C. It leverages CocoaTouch modules and is less invasive than CocoaPods.
144 |
145 | To install with carthage, follow the instruction on [Carthage](https://github.com/Carthage/Carthage)
146 |
147 | #### Cartfile
148 | ```
149 | github "hipposan/LemonDeer"
150 | ```
151 |
152 | ### Installation with Swift Package Manager
153 | The Swift Package Manager is a tool for managing the distribution of Swift code. Just add the url of this repo to your `Package.swift` file as a dependency:
154 |
155 | ```swift
156 | import PackageDescription
157 |
158 | let package = Package(
159 | name: "YourPackage",
160 | dependencies: [
161 | .Package(url: "https://github.com/hipposan/LemonDeer.git", majorVersion: 1.0.0)
162 | ]
163 | )
164 | ```
165 |
166 | ## Author
167 | Contact me at [Twitter](https://twitter.com/zzy0600).
168 |
169 |
170 | ## License
171 | LemonDeer is available under the MIT license. See the LICENSE file for more info.
172 |
--------------------------------------------------------------------------------
/Resources/LemonDeer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hipposan/LemonDeer/bfb0d9a6225ca9eaf0cec525033ddcb45083325d/Resources/LemonDeer-logo.png
--------------------------------------------------------------------------------
/Sources/DownloadManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadManager.swift
3 | // Demo
4 | //
5 | // Created by Ziyi Zhang on 14/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol DownloadManagerDelegate {
12 | func downloadSucceeded(with downloadManager: DownloadManager)
13 | func downloadFailed(with downloadManager: DownloadManager)
14 | }
15 |
16 | public class DownloadManager: NSObject {
17 | var fileName: String = ""
18 | var filePath: String = ""
19 |
20 |
21 |
22 | var delegate: DownloadManagerDelegate?
23 |
24 | func generateFilePath() -> URL {
25 | return getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(filePath).appendingPathComponent(fileName)
26 | }
27 | }
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Sources/LemonDeer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LemonDeer.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol LemonDeerDelegate: class {
12 | func videoDownloadSucceeded()
13 | func videoDownloadFailed()
14 |
15 | func updateProgressLabel(by percentage: String)
16 | }
17 |
18 | open class LemonDeer {
19 | public let downloader = VideoDownloader()
20 | let m3u8Parser = M3u8Parser()
21 | var playURL = ""
22 | var isM3u8 = false
23 |
24 | public weak var delegate: LemonDeerDelegate?
25 |
26 | public init(directoryName: String) {
27 | m3u8Parser.identifier = directoryName
28 | }
29 |
30 | open func parse(m3u8URL: String) {
31 | downloader.delegate = self
32 | m3u8Parser.delegate = self
33 | m3u8Parser.parse(with: m3u8URL)
34 |
35 | playURL = m3u8URL
36 | }
37 | }
38 |
39 | extension LemonDeer: M3u8ParserDelegate {
40 | func parseM3u8Succeeded(by parser: M3u8Parser) {
41 | downloader.tsPlaylist = parser.tsPlaylist
42 | downloader.m3u8Data = parser.m3u8Data
43 | downloader.startDownload()
44 | }
45 |
46 | func parseM3u8Failed(by parser: M3u8Parser) {
47 | print("Parse m3u8 file failed.")
48 | }
49 | }
50 |
51 | extension LemonDeer: VideoDownloaderDelegate {
52 | func videoDownloadSucceeded(by downloader: VideoDownloader) {
53 | delegate?.videoDownloadSucceeded()
54 | }
55 |
56 | func videoDownloadFailed(by downloader: VideoDownloader) {
57 | delegate?.videoDownloadFailed()
58 | }
59 |
60 | func updateProgressLabel(by percentage: String) {
61 | delegate?.updateProgressLabel(by: percentage)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/M3u8Parser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // m3u8Handler.swift
3 | // WindmillComic
4 | //
5 | // Created by hippo_san on 08/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol M3u8ParserDelegate: class {
12 | func parseM3u8Succeeded(by parser: M3u8Parser)
13 | func parseM3u8Failed(by parser: M3u8Parser)
14 | }
15 |
16 | open class M3u8Parser {
17 | weak var delegate: M3u8ParserDelegate?
18 |
19 | var m3u8Data: String = ""
20 | var tsSegmentArray = [M3u8TsSegmentModel]()
21 | var tsPlaylist = M3u8Playlist()
22 | var identifier = ""
23 |
24 | /**
25 | To parse m3u8 file with a provided URL.
26 |
27 | - parameter url: A string of URL you want to parse.
28 | */
29 | open func parse(with url: String) {
30 | guard let m3u8ParserDelegate = delegate else {
31 | print("M3u8ParserDelegate not set.")
32 | return
33 | }
34 |
35 | if !(url.hasPrefix("http://") || url.hasPrefix("https://")) {
36 | print("Invalid URL.")
37 | m3u8ParserDelegate.parseM3u8Failed(by: self)
38 | return
39 | }
40 |
41 | do {
42 | let m3u8Content = try String(contentsOf: URL(string: url)!, encoding: .utf8)
43 |
44 | if m3u8Content == "" {
45 | print("Empty m3u8 content.")
46 | m3u8ParserDelegate.parseM3u8Failed(by: self)
47 | return
48 | } else {
49 | guard (m3u8Content.range(of: "#EXTINF:") != nil) else {
50 | print("No EXTINF info.")
51 | m3u8ParserDelegate.parseM3u8Failed(by: self)
52 | return
53 | }
54 |
55 | self.m3u8Data = m3u8Content
56 | if tsSegmentArray.count > 0 { tsSegmentArray.removeAll() }
57 |
58 | let segmentRange = m3u8Content.range(of: "#EXTINF:")!
59 | let segmentsString = String(m3u8Content.characters.suffix(from: segmentRange.lowerBound)).components(separatedBy: "#EXT-X-ENDLIST")
60 | var segmentArray = segmentsString[0].components(separatedBy: "\r\n")
61 |
62 | if segmentArray.contains("#EXT-X-DISCONTINUITY") {
63 | segmentArray = segmentArray.filter { $0 != "#EXT-X-DISCONTINUITY" }
64 | }
65 |
66 | while (segmentArray.count > 2) {
67 | var segmentModel = M3u8TsSegmentModel()
68 |
69 | let segmentDurationPart = segmentArray[0].components(separatedBy: ":")[1]
70 | var segmentDuration: Float = 0.0
71 |
72 | if segmentDurationPart.contains(",") {
73 | segmentDuration = Float(segmentDurationPart.components(separatedBy: ",")[0])!
74 | } else {
75 | segmentDuration = Float(segmentDurationPart)!
76 | }
77 |
78 | let segmentURL = segmentArray[1]
79 | segmentModel.duration = segmentDuration
80 | segmentModel.locationURL = segmentURL
81 |
82 | tsSegmentArray.append(segmentModel)
83 |
84 | segmentArray.remove(at: 0)
85 | segmentArray.remove(at: 0)
86 | }
87 |
88 | tsPlaylist.initSegment(with: tsSegmentArray)
89 | tsPlaylist.identifier = identifier
90 |
91 | m3u8ParserDelegate.parseM3u8Succeeded(by: self)
92 | }
93 | } catch let error {
94 | print(error.localizedDescription)
95 | print("Read m3u8 file content error.")
96 | }
97 |
98 |
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/M3u8Playlist.swift:
--------------------------------------------------------------------------------
1 | //
2 | // M3u8Playlist.swift
3 | // WindmillComic
4 | //
5 | // Created by hippo_san on 08/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class M3u8Playlist {
12 | var tsSegmentArray = [M3u8TsSegmentModel]()
13 | var length = 0
14 | var identifier = ""
15 |
16 | func initSegment(with array: [M3u8TsSegmentModel]) {
17 | tsSegmentArray = array
18 | length = array.count
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/M3u8TsSegmentModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // M3u8TsSegmentModel.swift
3 | // WindmillComic
4 | //
5 | // Created by hippo_san on 08/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct M3u8TsSegmentModel {
12 | var duration: Float = 0.0
13 | var locationURL = ""
14 | var index: Int = 0
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/SegmentDownloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SegmentDownloader.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol SegmentDownloaderDelegate {
12 | func segmentDownloadSucceeded(with downloader: SegmentDownloader)
13 | func segmentDownloadFailed(with downloader: SegmentDownloader)
14 | }
15 |
16 | class SegmentDownloader: NSObject {
17 | var fileName: String
18 | var filePath: String
19 | var downloadURL: String
20 | var duration: Float
21 | var index: Int
22 |
23 | lazy var downloadSession: URLSession = {
24 | let configuration = URLSessionConfiguration.default
25 | let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
26 |
27 | return session
28 | }()
29 |
30 | var downloadTask: URLSessionDownloadTask?
31 | var resumeData: Data?
32 | var isDownloading = false
33 |
34 | var delegate: SegmentDownloaderDelegate?
35 |
36 | init(with url: String, filePath: String, fileName: String, duration: Float, index: Int) {
37 | downloadURL = url
38 | self.filePath = filePath
39 | self.fileName = fileName
40 | self.duration = duration
41 | self.index = index
42 | }
43 |
44 | func startDownload() {
45 | if checkIfIsDownloaded() {
46 | delegate?.segmentDownloadSucceeded(with: self)
47 | return
48 | } else {
49 | downloadTask = downloadSession.downloadTask(with: URL(string: downloadURL)!)
50 | downloadTask?.resume()
51 | isDownloading = true
52 | }
53 | }
54 |
55 | func cancelDownload() {
56 | downloadTask?.cancel()
57 | isDownloading = false
58 | }
59 |
60 | func pauseDownload() {
61 | if isDownloading {
62 | downloadTask?.suspend()
63 |
64 | isDownloading = false
65 | }
66 | }
67 |
68 | func resumeDownload() {
69 | downloadTask?.resume()
70 | isDownloading = true
71 | }
72 |
73 | func checkIfIsDownloaded() -> Bool {
74 | let filePath = generateFilePath().path
75 |
76 | if FileManager.default.fileExists(atPath: filePath) {
77 | return true
78 | } else {
79 | return false
80 | }
81 | }
82 |
83 | func generateFilePath() -> URL {
84 | return getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(filePath).appendingPathComponent(fileName)
85 | }
86 | }
87 |
88 | extension SegmentDownloader: URLSessionDownloadDelegate {
89 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
90 | let destinationURL = generateFilePath()
91 |
92 | if FileManager.default.fileExists(atPath: destinationURL.path) {
93 | delegate?.segmentDownloadSucceeded(with: self)
94 | return
95 | } else {
96 | do {
97 | try FileManager.default.moveItem(at: location, to: destinationURL)
98 | delegate?.segmentDownloadSucceeded(with: self)
99 | } catch let error as NSError {
100 | print(error.localizedDescription)
101 | }
102 | }
103 | }
104 |
105 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
106 | if error != nil {
107 | delegate?.segmentDownloadFailed(with: self)
108 | }
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/VideoDownloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoDownloader.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol VideoDownloaderDelegate {
12 | func videoDownloadSucceeded(by downloader: VideoDownloader)
13 | func videoDownloadFailed(by downloader: VideoDownloader)
14 |
15 | func updateProgressLabel(by percentage: String)
16 | }
17 |
18 | open class VideoDownloader {
19 | var m3u8Data: String = ""
20 | var tsPlaylist = M3u8Playlist()
21 | var segmentDownloaders = [SegmentDownloader]()
22 | var tsFilesIndex = 0
23 | var downloadedTsFilesCount = 0
24 | var neededDownloadTsFilesCount = 0
25 | var downloadURLs = [String]()
26 | var downloadingProgress: String {
27 | let fraction: Float = Float((Float(downloadedTsFilesCount) / Float(neededDownloadTsFilesCount)) * 100)
28 | let roundedValue: Int = Int(fraction.rounded(.toNearestOrEven))
29 | let progressString = roundedValue.description + " %"
30 |
31 | return progressString
32 | }
33 |
34 | var delegate: VideoDownloaderDelegate?
35 |
36 | open func startDownload() {
37 | checkOrCreatedM3u8Directory()
38 |
39 | var newSegmentArray = [M3u8TsSegmentModel]()
40 |
41 | let notInDownloadList = tsPlaylist.tsSegmentArray.filter { !downloadURLs.contains($0.locationURL) }
42 | neededDownloadTsFilesCount = notInDownloadList.count
43 |
44 | for i in 0 ..< notInDownloadList.count {
45 | let fileName = "\(tsFilesIndex).ts"
46 |
47 | let segmentDownloader = SegmentDownloader(with: notInDownloadList[i].locationURL,
48 | filePath: tsPlaylist.identifier,
49 | fileName: fileName,
50 | duration: notInDownloadList[i].duration,
51 | index: tsFilesIndex)
52 | segmentDownloader.delegate = self
53 |
54 | segmentDownloaders.append(segmentDownloader)
55 | downloadURLs.append(notInDownloadList[i].locationURL)
56 |
57 | var segmentModel = M3u8TsSegmentModel()
58 | segmentModel.duration = segmentDownloaders[i].duration
59 | segmentModel.locationURL = segmentDownloaders[i].fileName
60 | segmentModel.index = segmentDownloaders[i].index
61 | newSegmentArray.append(segmentModel)
62 |
63 | tsPlaylist.tsSegmentArray = newSegmentArray
64 |
65 | segmentDownloaders[i].startDownload()
66 |
67 | tsFilesIndex += 1
68 | }
69 | }
70 |
71 | func updateLocalM3U8file() {
72 | checkOrCreatedM3u8Directory()
73 |
74 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("\(tsPlaylist.identifier).m3u8")
75 |
76 | var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n"
77 | var content = ""
78 |
79 | for i in 0 ..< tsPlaylist.tsSegmentArray.count {
80 | let segmentModel = tsPlaylist.tsSegmentArray[i]
81 |
82 | let length = "#EXTINF:\(segmentModel.duration),\n"
83 | let fileName = "http://127.0.0.1:8080/\(segmentModel.index).ts\n"
84 | content += (length + fileName)
85 | }
86 |
87 | header.append(content)
88 | header.append("#EXT-X-ENDLIST\n")
89 |
90 | let writeData: Data = header.data(using: .utf8)!
91 | try! writeData.write(to: filePath)
92 | }
93 |
94 | private func checkOrCreatedM3u8Directory() {
95 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(tsPlaylist.identifier)
96 |
97 | if !FileManager.default.fileExists(atPath: filePath.path) {
98 | try! FileManager.default.createDirectory(at: filePath, withIntermediateDirectories: true, attributes: nil)
99 | }
100 | }
101 |
102 | open func deleteAllDownloadedContents() {
103 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").path
104 |
105 | if FileManager.default.fileExists(atPath: filePath) {
106 | try! FileManager.default.removeItem(atPath: filePath)
107 | } else {
108 | print("File has already been deleted.")
109 | }
110 | }
111 |
112 | open func deleteDownloadedContents(with name: String) {
113 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(name).path
114 |
115 | if FileManager.default.fileExists(atPath: filePath) {
116 | try! FileManager.default.removeItem(atPath: filePath)
117 | } else {
118 | print("Could not find directory with name: \(name)")
119 | }
120 | }
121 |
122 | open func pauseDownloadSegment() {
123 | _ = segmentDownloaders.map { $0.pauseDownload() }
124 | }
125 |
126 | open func cancelDownloadSegment() {
127 | _ = segmentDownloaders.map { $0.cancelDownload() }
128 | }
129 |
130 | open func resumeDownloadSegment() {
131 | _ = segmentDownloaders.map { $0.resumeDownload() }
132 | }
133 | }
134 |
135 | extension VideoDownloader: SegmentDownloaderDelegate {
136 | func segmentDownloadSucceeded(with downloader: SegmentDownloader) {
137 | downloadedTsFilesCount += 1
138 |
139 | DispatchQueue.main.async {
140 | self.delegate?.updateProgressLabel(by: self.downloadingProgress)
141 | }
142 |
143 | updateLocalM3U8file()
144 |
145 | if downloadedTsFilesCount == neededDownloadTsFilesCount {
146 | delegate?.videoDownloadSucceeded(by: self)
147 | }
148 | }
149 |
150 | func segmentDownloadFailed(with downloader: SegmentDownloader) {
151 | delegate?.videoDownloadFailed(by: self)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Sources/VideoDownloaderHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoDownloaderHelper.swift
3 | // WindmillComic
4 | //
5 | // Created by Ziyi Zhang on 09/06/2017.
6 | // Copyright © 2017 Ziyideas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func getDocumentsDirectory() -> URL {
12 | let paths = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask)
13 | let documentsDirectory = paths[0]
14 | return documentsDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/_Pods.xcodeproj:
--------------------------------------------------------------------------------
1 | Example/Pods/Pods.xcodeproj
--------------------------------------------------------------------------------