Skip to content

Commit

Permalink
✨ HLS 세그먼트 4초만 로드하도록 개선
Browse files Browse the repository at this point in the history
  • Loading branch information
loinsir committed Dec 13, 2023
1 parent 616829c commit 3a10a0f
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 17 deletions.
26 changes: 22 additions & 4 deletions iOS/Layover/Layover.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@
19AE481C2B28C53800DD4612 /* MockSettingWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19AE481B2B28C53800DD4612 /* MockSettingWorker.swift */; };
19AE48232B29D03D00DD4612 /* EditProfileInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19AE481F2B29D03D00DD4612 /* EditProfileInteractorTests.swift */; };
19AE48252B29D03D00DD4612 /* EditProfilePresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19AE48212B29D03D00DD4612 /* EditProfilePresenterTests.swift */; };
19AE482A2B2A127E00DD4612 /* HLSAssetResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19AE48292B2A127E00DD4612 /* HLSAssetResourceLoaderDelegate.swift */; };
19AE482C2B2A1A8B00DD4612 /* HLSSliceResourceLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19AE482B2B2A1A8B00DD4612 /* HLSSliceResourceLoader.swift */; };
19AE482E2B2A24C700DD4612 /* URL+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19AE482D2B2A24C700DD4612 /* URL+.swift */; };
19C7AFCE2B02410F003B35F2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C7AFCD2B02410F003B35F2 /* AuthManager.swift */; };
19C7AFD62B02584D003B35F2 /* KeychainStored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C7AFD52B02584D003B35F2 /* KeychainStored.swift */; };
19E79AC02B0A85D0009EA9ED /* LoopingPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E79ABF2B0A85D0009EA9ED /* LoopingPlayerView.swift */; };
Expand Down Expand Up @@ -361,6 +364,9 @@
19AE481B2B28C53800DD4612 /* MockSettingWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSettingWorker.swift; sourceTree = "<group>"; };
19AE481F2B29D03D00DD4612 /* EditProfileInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileInteractorTests.swift; sourceTree = "<group>"; };
19AE48212B29D03D00DD4612 /* EditProfilePresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePresenterTests.swift; sourceTree = "<group>"; };
19AE48292B2A127E00DD4612 /* HLSAssetResourceLoaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSAssetResourceLoaderDelegate.swift; sourceTree = "<group>"; };
19AE482B2B2A1A8B00DD4612 /* HLSSliceResourceLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSSliceResourceLoader.swift; sourceTree = "<group>"; };
19AE482D2B2A24C700DD4612 /* URL+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+.swift"; sourceTree = "<group>"; };
19C7AFCD2B02410F003B35F2 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
19C7AFD52B02584D003B35F2 /* KeychainStored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStored.swift; sourceTree = "<group>"; };
19E79ABF2B0A85D0009EA9ED /* LoopingPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopingPlayerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -646,8 +652,6 @@
194C21CE2B1DF63D00C62645 /* MockDatas */ = {
isa = PBXGroup;
children = (
194C21D32B1EEE3700C62645 /* sample.jpeg */,
194C21CF2B1DF65200C62645 /* PostList.json */,
FC4E0C122B28609C00152596 /* PostBoard.json */,
192513972B278645001533FA /* CheckSignUp.json */,
1925138D2B278645001533FA /* CheckUserName.json */,
Expand Down Expand Up @@ -778,6 +782,15 @@
path = EditProfile;
sourceTree = "<group>";
};
19AE48262B2A117600DD4612 /* HLSResourceLoader */ = {
isa = PBXGroup;
children = (
19AE482B2B2A1A8B00DD4612 /* HLSSliceResourceLoader.swift */,
19AE48292B2A127E00DD4612 /* HLSAssetResourceLoaderDelegate.swift */,
);
path = HLSResourceLoader;
sourceTree = "<group>";
};
19BB8A572B07BEE30070B922 /* UIComponents */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1080,6 +1093,7 @@
FC7E45752AFF6F5B004F155A /* Services */ = {
isa = PBXGroup;
children = (
19AE48262B2A117600DD4612 /* HLSResourceLoader */,
1972CCD02B125E8800C3C762 /* UserDefaults */,
19C7AFD42B02583C003B35F2 /* Keychain */,
FC4E0C172B28954000152596 /* Location */,
Expand All @@ -1101,10 +1115,10 @@
FC7E457B2AFF6F9D004F155A /* Scenes */ = {
isa = PBXGroup;
children = (
8321A2E72B1E1011000A12AF /* Report */,
1945520E2B03AEA400299768 /* Configurator.swift */,
836C33922B18436A00ECAFB0 /* Setting */,
19BB8A572B07BEE30070B922 /* UIComponents */,
8321A2E72B1E1011000A12AF /* Report */,
836C33922B18436A00ECAFB0 /* Setting */,
835A61962B0680FC002F22A5 /* Playback */,
FC2511A72B04DA9C004717BC /* Map */,
FCEE0FFB2B03AFAA00195BBE /* SignUpScene */,
Expand Down Expand Up @@ -1151,6 +1165,7 @@
FC767FA42B125F430088CF9B /* UIViewController+.swift */,
1972CCDE2B14C9B000C3C762 /* Notification.Name+.swift */,
19A169482B181AE300DB34C0 /* Sequence+.swift */,
19AE482D2B2A24C700DD4612 /* URL+.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -1379,6 +1394,7 @@
FC8696D32B26008B00F9A7B9 /* SettingViewController.swift in Sources */,
83C35E1E2B10923C00D8DD5C /* PlaybackCell.swift in Sources */,
FC0E80262B1A0BBB00EF56D6 /* UploadPostRouter.swift in Sources */,
19AE482A2B2A127E00DD4612 /* HLSAssetResourceLoaderDelegate.swift in Sources */,
FC2511A42B045D6C004717BC /* SignUpModels.swift in Sources */,
8321A2FD2B1E4260000A12AF /* DefaultPostManagerEndPointFactory.swift in Sources */,
FC767F932B1220CC0088CF9B /* NicknameDTO.swift in Sources */,
Expand All @@ -1394,6 +1410,7 @@
FC0E803A2B1B91C900EF56D6 /* EditTagPresenter.swift in Sources */,
836C33872B15A29600ECAFB0 /* Toast.swift in Sources */,
FC767F972B1224B80088CF9B /* IntroduceDTO.swift in Sources */,
19AE482E2B2A24C700DD4612 /* URL+.swift in Sources */,
19A169302B1776CA00DB34C0 /* TagPlayListCollectionViewCell.swift in Sources */,
FC0E80252B1A0BBB00EF56D6 /* UploadPostWorker.swift in Sources */,
1972CCD42B138E6B00C3C762 /* SignUpRouter.swift in Sources */,
Expand Down Expand Up @@ -1532,6 +1549,7 @@
1972CCCF2B12438900C3C762 /* LoginEndPointFactory.swift in Sources */,
835A61A92B0B5A31002F22A5 /* LoginConfigurator.swift in Sources */,
FC0E80432B1B934A00EF56D6 /* EditTagConfigurator.swift in Sources */,
19AE482C2B2A1A8B00DD4612 /* HLSSliceResourceLoader.swift in Sources */,
194551F72B037F2D00299768 /* LoginInteractor.swift in Sources */,
19A169382B17BCA800DB34C0 /* PostDTO.swift in Sources */,
19A1692A2B176D6E00DB34C0 /* TagPlayListConfigurator.swift in Sources */,
Expand Down
25 changes: 25 additions & 0 deletions iOS/Layover/Layover/Extensions/URL+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// URL+.swift
// Layover
//
// Created by 김인환 on 12/14/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import Foundation

extension URL {
func changeScheme(to: String) -> URL {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
components?.scheme = to
return components?.url ?? self
}

var customHLS_URL: URL {
changeScheme(to: "lhls")
}

var originHLS_URL: URL {
changeScheme(to: "https")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ final class HomeCarouselCollectionViewCell: UICollectionViewCell {

func setVideo(url: URL, loopingAt time: TimeInterval) {
loopingPlayerView.disable()
loopingPlayerView.prepareVideo(with: url, loopStart: time, duration: 3.0)
loopingPlayerView.prepareVideo(with: url,
assetResourceLoaderDelegate: HLSAssetResourceLoaderDelegate(resourceLoader: HLSSliceResourceLoader()),
loopStart: time,
duration: 3.0)
loopingPlayerView.player?.isMuted = true
}

Expand Down
1 change: 0 additions & 1 deletion iOS/Layover/Layover/Scenes/Home/HomeConfigurator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ final class HomeConfigurator: Configurator {
let router = HomeRouter()
let presenter = HomePresenter()
let interactor = HomeInteractor()
// let homeWorker = MockHomeWorker()
let homeWorker = HomeWorker()
let videoFileWorker = VideoFileWorker()

Expand Down
39 changes: 28 additions & 11 deletions iOS/Layover/Layover/Scenes/UIComponents/LoopingPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ final class LoopingPlayerView: UIView {
player?.timeControlStatus == .playing
}

private var assetResourceLoaderDelegate: AVAssetResourceLoaderDelegate?

// MARK: - View Life Cycle

override func layoutSubviews() {
Expand All @@ -45,8 +47,23 @@ final class LoopingPlayerView: UIView {
self.player = player
}

func prepareVideo(with url: URL, loopStart: TimeInterval, duration: TimeInterval) {
let playerItem = AVPlayerItem(url: url)
func prepareVideo(with url: URL,
assetResourceLoaderDelegate: AVAssetResourceLoaderDelegate? = nil,
loopStart: TimeInterval,
duration: TimeInterval) {
let asset: AVURLAsset
if let assetResourceLoaderDelegate {
self.assetResourceLoaderDelegate = assetResourceLoaderDelegate
asset = AVURLAsset(url: url.customHLS_URL)
asset.resourceLoader.setDelegate(assetResourceLoaderDelegate,
queue: DispatchQueue.global(qos: .utility))
Task {
try await asset.load(.isPlayable, .duration)
}
} else {
asset = AVURLAsset(url: url)
}
let playerItem = AVPlayerItem(asset: asset)
let player = AVQueuePlayer()
looper = AVPlayerLooper(player: player,
templateItem: playerItem,
Expand Down Expand Up @@ -75,12 +92,12 @@ final class LoopingPlayerView: UIView {
}
}

#Preview {
let view = LoopingPlayerView()
view.prepareVideo(with: URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!,
timeRange: .init(start: .zero,
end: .init(seconds: 3.0, preferredTimescale: CMTimeScale(1.0))))
view.play()
view.player?.isMuted = true
return view
}
//#Preview {
// let view = LoopingPlayerView()
// view.prepareVideo(with: URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!,
// timeRange: .init(start: .zero,
// end: .init(seconds: 3.0, preferredTimescale: CMTimeScale(1.0))))
// view.play()
// view.player?.isMuted = true
// return view
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// HLSAssetResourceLoaderDelegate.swift
// Layover
//
// Created by 김인환 on 12/14/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import Foundation
import AVFoundation

class HLSAssetResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate {

// MARK: - Properties

let resourceLoader: ResourceLoader

// MARK: - Initializer

init(resourceLoader: ResourceLoader) {
self.resourceLoader = resourceLoader
}

// MARK: - Delegate Methods

func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
loadRequestedResource(loadingRequest)
}

func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool {
loadRequestedResource(renewalRequest)
}

// MARK: - Methods

// 공통으로 처리
func loadRequestedResource(_ loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard let url = loadingRequest.request.url?.originHLS_URL else { return false }

if url.pathExtension.contains("ts") { // ts 파일은 리디렉션 시킨다.
loadingRequest.redirect = URLRequest(url: url)
loadingRequest.response = HTTPURLResponse(url: url,
statusCode: 302,
httpVersion: nil,
headerFields: nil)
loadingRequest.finishLoading()
} else {
Task {
guard let data = await resourceLoader.loadResource(from: url) else {
loadingRequest.finishLoading(with: NSError(domain: "Failed to load resource from \(url.absoluteString)",
code: 0,
userInfo: nil))
return
}

loadingRequest.dataRequest?.respond(with: data)
loadingRequest.finishLoading()
}
}

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// HLSResourceLoader.swift
// Layover
//
// Created by 김인환 on 12/14/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import Foundation
import OSLog

protocol ResourceLoader {
func loadResource(from url: URL) async -> Data?
}

// 앞부분부터 원하는 duration만큼 잘라서 load시켜주는 Resource Loader
final class HLSSliceResourceLoader: ResourceLoader {

enum M3U8Tag: String {
case extm3u = "#EXTM3U" // m3u8 파일의 시작
case extend = "#EXT-X-ENDLIST" // 마지막 태그
case extinf = "#EXTINF:" // 재생시간 -> 미디어 m3u8 파일에 포함
case extxstreaminf = "#EXT-X-STREAM-INF" // 마스터 m3u8 파일
}

// MARK: - Properties

private let session: URLSession

// MARK: - Initializer

init(session: URLSession = URLSession(configuration: .default)) {
self.session = session
}

// MARK: - ResourceLoader

func loadResource(from url: URL) async -> Data? {
let urlRequest = URLRequest(url: url.originHLS_URL) // 원래 url scheme 으로 변경

guard let (data, response) = try? await session.data(for: urlRequest),
let httpResponse = response as? HTTPURLResponse,
(200...399) ~= httpResponse.statusCode else {
os_log(.error, log: .data, "Failed to load resource from %{public}@", url.absoluteString)
return nil
}

guard let m3u8Playlist = String(data: data, encoding: .utf8) else {
os_log(.error, log: .data, "Failed to decode data to String")
return nil
}

guard isMediaM3U8(m3u8Playlist) else { return data }
return sliceM3U8Playlist(m3u8Playlist, duration: 4).data(using: .utf8) ?? data // 3초보다는 약간 여유있게 잡는다.
}

// MARK: - Methods

private func isMediaM3U8(_ m3u8Playlist: String) -> Bool {
m3u8Playlist.contains(M3U8Tag.extinf.rawValue)
}

// m3u8 미디어 플레이리스트를 받아서 duration만큼 잘라서 반환
private func sliceM3U8Playlist(_ m3u8Playlist: String, duration: TimeInterval) -> String {
var duration = duration
var playlist = m3u8Playlist.components(separatedBy: M3U8Tag.extinf.rawValue)
.compactMap {
if $0.contains(M3U8Tag.extm3u.rawValue) { return $0 } // 시작부분
else if let tsDuration = $0.components(separatedBy: ",").compactMap({ Double($0) }).first,
duration > .zero {
duration -= tsDuration
return $0
} else {
return nil
}
}.joined(separator: M3U8Tag.extinf.rawValue)

if !playlist.contains(M3U8Tag.extend.rawValue) {
playlist.append("\n\(M3U8Tag.extend.rawValue)")
}

return playlist
}
}

0 comments on commit 3a10a0f

Please sign in to comment.