[Swift] TwitchClone ๐Ÿชค์‚ฝ์งˆ1. HLS -> Thumbnail

๋ฒˆ๋“ฏํ•œ ํ˜ธ๋ž‘์ดยท2023๋…„ 11์›” 27์ผ

TwitchClone

๋ชฉ๋ก ๋ณด๊ธฐ
2/3

[Swift] TwitchClone ๐Ÿชค์‚ฝ์งˆ1. HLS -> Thumbnail

"๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๊ฐœ์ธํ”„๋กœ์ ํŠธ์ด๋ฉฐ ๋ณธ์ธ์ด ํฌ๊ฒŒ ์‚ฝ์งˆํ•œ ๋ถ€๋ถ„์„ ์œ„์ฃผ๋กœ ์ •๋ฆฌ, ๊ธฐ๋ก์šฉ์œผ๋กœ ์ž‘์„ฑ๋ฉ๋‹ˆ๋‹ค."

์‹œ์ฒญ์ž ํ™”๋ฉด์—์„œ Live List๋ฅผ ๋ณผ ๋•Œ ์‹œ๊ฐ์ ์ธ ์ •๋ณด์ธ ์ธ๋„ค์ผ์ด ํ•„์š”ํ–ˆ์Œ.
์ผ๋‹จ ๋– ์˜ค๋ฅด๋Š” ๋ฐฉ๋ฒ•์œผ๋ก  URL(.m3u8)์—์„œ ๋”ฐ์˜ค๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์•„ ์—ฌ๋Ÿฌ ์‹œ๋„๋ฅผ ํ•ด๋ณด์•˜์Œ.

์‹œ๋„ 1. AVAssetImageGenerator

Apple ๊ณต์‹๋ฌธ์„œ์—์„œ ์ œ๊ณตํ•˜๋Š” HTTP Live Streaming (HLS) ํ˜•์‹์„ cgImage ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉ.
AVPlayer์—์„œ ์ „์ฒด ์‹œ๊ฐ„์ด ๋œจ๋Š” m3u8 ํ˜•์‹์˜ URL์€ ์ž˜ ์ž‘๋™ํ•จ.
๊ทธ๋Ÿฐ๋ฐ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” Live ํ˜•์‹์€ image๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Œ. (๋‚ด๊ฐ€ ๋ชปํ•œ๊ฑธ์ˆ˜๋„)

์‹คํŒจ.

์‹œ๋„ 2. AVPlayerLayer

๊ฐ Cell์— ์žˆ๋Š” AVPlayerLayer์— URL์„ ์ฃผ๊ณ  ์žฌ์ƒ ์ƒํƒœ๋ฅผ ์ •์ง€ ํ•ด๋†“์œผ๋ฉด ์ธ๋„ค์ผ ๋А๋‚Œ์„ ๋‚ผ ์ˆ˜ ์žˆ์—ˆ์Œ.
๊ทผ๋ฐ ๋งค์šฐ ๋А๋ฆฌ๊ฑฐ๋‚˜ ๊ฐ ์˜์ƒ๋งˆ๋‹ค ๋”œ๋ ˆ์ด๊ฐ€ ๋‹ฌ๋ผ ๋œฐ๋•Œ๋„ ์žˆ๊ณ  ์•ˆ๋œฐ๋•Œ๋„ ์žˆ์—ˆ์Œ.
๊ทธ๋ฆฌ๊ณ  Cell์„ ๋‹ค์‹œ ๋กœ๋“œํ•  ๋•Œ ๋‹ค๋ฅธ ์˜์ƒ์˜ ์ธ๋„ค์ผ์ด ํ‘œ์‹œ๋˜๋Š” ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ์Œ.
์ธ๋„ค์ผ์„ ํ‘œ์‹œํ•œ๋‹ค๋Š” ๋ชฉ์ ์€ ๋‹ฌ์„ฑํ–ˆ์ง€๋งŒ, ๋งˆ์Œ์— ์•ˆ๋“ค์—ˆ์Œ.

์‹คํŒจ?

์‹œ๋„ 3. ACThumbnailGenerator

์œ„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” HLS/.m3u8๋ฅผ UIImage๋กœ ์ถ”์ถœํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž„.
UIImage๋กœ ์ถ”์ถœํ•˜๋Š” ์‹œ๊ฐ„์ด ๋‹ค์†Œ ์žˆ๊ธฐ์— Image Cache ์ฒ˜๋ฆฌ๋กœ ๋‹ค์‹œ ๋กœ๋“œํ•  ๋•Œ ์‹œ๊ฐ„์„ ์ค„์˜€์Œ.
๊ทผ๋ฐ ์ฒซ Image ์ถ”์ถœ์ด ์•ฝ 0.5 ~ 1์ดˆ ์ •๋„ ๊ฑธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ Cell์— ์žˆ๋Š” Image๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ์—๋Š” ๋ถ€์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จ. (๋‚ด๊ฐ€ ์‚ฌ์šฉ์ž์˜€์Œ ์†ํ„ฐ์กŒ์„๋“ฏ)

์‹คํŒจ.

์‹œ๋„ 4. ์„ฑ๊ณต?

์ผ๋‹จ ์ ‘๊ทผ๋ฐฉ์‹์ด ์ž˜๋ชป๋œ๊ฒƒ ๊ฐ™์•„ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•จ.
์‹œ์ฒญ์ž์ชฝ์—์„œ ์ธ๋„ค์ผ์„ ๋งŒ๋“œ๋Š”๊ฒƒ์€ ๋„ˆ๋ฌด ๋น„ํšจ์œจ์ ์ด๋ผ ํŒ๋‹จ.
๊ทธ๋Ÿผ ์„œ๋ฒ„์—์„œ ์ธ๋„ค์ผ์„ ๋งŒ๋“ค์–ด ๋ณด๋‚ด์ฃผ๋Š” ๋ฐฉ๋ฒ•, Host์ชฝ์—์„œ ์ธ๋„ค์ผ์„ ์ œ์ž‘ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ๋– ์˜ค๋ฆ„.
์„œ๋ฒ„์—์„œ ์ธ๋„ค์ผ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์€ RTMP๋ฅผ ๋ฐ›๋Š” Nginx์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š”๋ฐ ๋˜ ์—„์ฒญ๋‚œ ์‚ฝ์งˆ์ด ์˜ˆ์ƒ๋ ๊ฒƒ ๊ฐ™์•„ ์•ž์„  ์‚ฝ์งˆ๋กœ ๋ชธ๊ณผ ๋งˆ์Œ์ง€ ์ง€์ฒ˜ ํฌ๊ธฐ.

Host์ชฝ์—์„œ ์ธ๋„ค์ผ์„ ์ œ์ž‘ํ•˜๋Š” ๋ฐฉ์‹์€ ์–ด๋А์ •๋„ ๊ฐ์ด ์žกํ˜€ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ํ•จ.

๋จผ์ € ์†ก์ถœ ๋ถ€๋ถ„์€ HaishinKit ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•จ.

class StreamCam: UIViewController {
	
    ...
    
    func startStream() {
        rtmpConnection.connect("rtmp://diddbstjr55.shop/hls") // ์†ก์ถœํ•  ๋ฏธ๋””์–ด์„œ๋ฒ„ ์ฃผ์†Œ
        rtmpStream.publish(userEmailSplit) // Host Key
        rtmpConnection.addEventListener(.rtmpStatus, selector: #selector(rtmpStatusHandler), observer: self) // ์†ก์ถœ๋œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด addEventListener
        if rtmpStream.receiveVideo { // ์†ก์ถœ๋œ ์˜์ƒ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š”๊ฐ€?
            var count = 0
            timer = Timer(timeInterval: 1, repeats: true, block: { _ in
                count += 1
                if  count > 4 {
                    self.createThumbnail() // 4์ดˆ๋งˆ๋‹ค ์ธ๋„ค์ผ ์ƒ์„ฑ ์‹œ๋„
                    count = 0
                }
            })
            RunLoop.current.add(timer!, forMode: .common)
        }
    }
    
     func createThumbnail()  {
        let liveResult = LiveResult()
        let url = liveResult.hlsURL(hls: userEmailSplit ) // ์ด ์ฝ”๋“œ๋Š” ์•ž์—์„œ ์ €์žฅ๋œ ์œ ์ € Email์„ ๊ฐ€์ ธ์˜ค๋Š” ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค. 
        generator = ACThumbnailGenerator(streamUrl: url) // ๊ฐ€์ ธ์˜จ ์œ ์ € Email์„ ๊ณ ์œ  Key๋กœ ์„ค์ •ํ•ด ACThumbnailGeneratorDelegat์—์„œ ์ƒ์„ฑ๋œ ์ธ๋„ค์ผ์„ Cache ์ฒ˜๋ฆฌ
        generator.delegate = self
        generator.captureImage(at: 30) 
        let cacheKey = NSString(string: url.description)
        if let cacheImage = ImageCachManager.shared.object(forKey: cacheKey) { // Cache Image๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Œ FirebaseStorage์— ์—…๋กœ๋“œ ํ•˜๊ณ  ํƒ€์ด๋จธ ์ค‘์ง€
            timer?.invalidate()
            FirebaseStorageManager.uploadImage(image: cacheImage, imageName: userEmailSplit) { URL in
                if let url = URL {
                    self.uploadFirebase(imageURL: url.description)
                }
            }
        }
    }
   }
    ...
    
extension StreamCam: ACThumbnailGeneratorDelegate {
    func generator(_ generator: ACThumbnailGenerator, didCapture image: UIImage, at position: Double) {
        let url = generator.streamUrl
        print("image to",url)
        let cacheKey = NSString(string: url.description)
        ImageCachManager.shared.setObject(image, forKey: cacheKey) // ์—ฌ๊ธฐ์„œ Cache ์ฒ˜๋ฆฌํ•œ image๋Š” FirebaseStorage์— ์˜ฌ๋ฆด ๋•Œ ์‚ฌ์šฉ
    }
}
    

๋จผ์ € startStream() ์„ค์ •๋œ ์†ก์ถœ์„ ์‹œ๋„.
์„ค์ •๋œ ํƒ€์ด๋จธ๋Š” 4์ดˆ๋งˆ๋‹ค createThumbnail() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœ.

createThumbnail() ํ•จ์ˆ˜๋Š” 4์ดˆ๋งˆ๋‹ค ์†ก์ถœ๋œ ์˜์ƒ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์œผ๋ฉด ํƒ€์ด๋จธ๋ฅผ ์ค‘์ง€ํ•˜๊ณ  URL์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ถ”์ถœ ํ›„ FirebaseStorage์— ์—…๋กœ๋“œ.

์†ก์ถœ(RTMP) -> Nginx -> HLS(.m3u8) -> HLS Thumbmail ์ถ”์ถœ -> Firebase์— ๋ฐฉ์†ก์ •๋ณด( Title, Category, HLS URL...) ์—…๋กœ๋“œ -> ์‚ฌ์šฉ์ž์—๊ฒŒ ๋…ธ์ถœ

๋‹จ์ ์€ RTMP์˜ ๋”œ๋ ˆ์ด๊ฐ€ ์–ด๋А์ •๋„ ์žˆ๊ธฐ์— Host๊ฐ€ ๋ฐฉ์†ก์„ ์‹œ์ž‘ํ•˜๊ณ  ์•ฝ 4~8์ดˆ ์ดํ›„ ์‹œ์ฒญ์ž์—๊ฒŒ ๊ณต๊ฐœ๋œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

๋‚ด๊ฐ€ ๊ฒฝํ—˜ํ•œ ์‹ค์ œ ์ธํ„ฐ๋„ท ๋ฐฉ์†ก์€ Host๊ฐ€ ๋ฐฉ์†ก์„ ์‹œ์ž‘ํ•˜๊ณ  ๋ณธ ๋‚ด์šฉ์œผ๋กœ ๋“ค์–ด๊ฐ€๊ธฐ์ „ ์•ฝ 5~10๋ถ„ ์ •๋„์˜ ๋Œ€๊ธฐ์‹œ๊ฐ„์„ ๊ฐ€์ง€๋Š”๊ฒƒ์œผ๋กœ ๋ชฉ๊ฒฉ๋˜์—ˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ์— ์‹ค์‚ฌ์šฉ์œผ๋ก  ํฐ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€๋Š” ์•Š์ง€๋งŒ ๊ตฌ์กฐ์ ์œผ๋กœ๋Š” ํšจ์œจ์ ์ด์ง€๋Š” ์•Š๋‹ค.

๊ฐœ์ธ์ ์œผ๋กœ๋Š” ์ด ๋ฐฉ๋ฒ•์ด ๋งˆ์Œ์— ๋“ค์ง€๋Š” ์•Š์ง€๋งŒ ์•„์ง ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์„ ์ฐพ์ง€ ๋ชปํ•ด ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

profile
์•ˆ๋…•ํ•˜์„ธ์š” iOS ํ™˜๊ฒฝ์—์„œ AI, Vision์„ ๊ณต๋ถ€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

5๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2024๋…„ 7์›” 6์ผ

์•ˆ๋…•ํ•˜์„ธ์š” ๊ธ€ ์ž˜์ฝ์—ˆ์Šต๋‹ˆ๋‹ค...
ํ˜น์‹œ ํ•™์ƒ์ด์‹ ๊ฑด๊ฐ€์š”...???

1๊ฐœ์˜ ๋‹ต๊ธ€