티스토리 뷰

2023.01.13 - [iOS개발/Swift 기본] - Swift 기본 기능으로 동영상 플레이하기

 

Swift 기본 기능으로 동영상 플레이하기

이번에는 동영상에 대해 올려볼까 합니다. 이번편은 iOS에서 기본적으로 사용할 수 있는 플레이어를 사용할 예정이고요. 다음 편에서는 플레이어를 직접 커스텀하는 부분을 올려볼까합니다. 참

world-of-larooly.tistory.com

이번 포스트에서 작성할 내용은 위 글의 다음 버전입니다. 

 

물론 기본으로 제공해주는 플레이어도 좋긴 하지만 한번 수작업(?)으로 만들어볼까합니다.  

일단 원리만 설명하면 일반 동영상 뷰어 위에 저희가 만든 버튼들로 제어를 하는 방식입니다. 

 

* 참고로 코드를 작성하시기 전에 아래 코드를 미리 추가해주세요.

import AVFoundation
import AVKit

단계를 나누어 하나씩 합시다. 

 

1. 기본 화면 구성 만들기 (스토리보드)

2. 뷰에 동영상 집어넣기 

3. 전체 버튼 UI 숨김 / 표시 기능 

4. 영상 시간 표시 제작 및 제어 (Slider)

5. 정지 / 재생 전환 버튼 제작

6. 10초 전 / 후 버튼 제작

7. 배속 재생 만들기 

8. 다듬기 (어색한 부분 고치기)

 

* 참고로 영상을 저장한게 아니라 인터넷 Url 로 작업하시면 렉이 잘 걸립니다.

* 그래서 저는 영상을 앱에 심어둔 상태에서 제작할 예정입니다. 

이렇게 심어두었다는 의미입니다.

* 전체 코드는 다음 포스트 한번에 올려드릴께요.

 

1. 기본 화면 구성 만들기 (스토리보드)

- 기본적으로 전체 버튼이 들어간 뷰를 구성한다고 생각하시면 됩니다.

- 하실 때 화면 영역 잡는 부분을 주의해 주세요!

 

1-1 화면 전체를 영역으로 잡기 (코드에서는 totalBtnsView 라고 정의할껍니다.)

- 육안으로 확인하기 편하도록 색상을 임의로 넣은 겁니다. 

- 배경 색상을 아래처럼 반 투명하게 넣어주세요. (그래야 영상 위에 버튼들이 있는것처럼 보이겠죠?)

- 이 뷰는 전체 뷰를 기준으로 만들어주세요. 그래야 영상 전체를 덮은 것처럼 보이겠죠?(safeArea X)

 

1-2 각 버튼의 위치 잡기 (원하시는 기능의 버튼들을 만들어주세요.)

- 위에 잡은 뷰안에 원하시는 위치에 원하는 모양의 버튼들을 추가해주세요. 

- 위치 잡으실때 safeArea 안쪽으로 잡아주시는게 좋아요! (그래야 버튼이 기본UI에 짤리거나 가려지는 경우를 막을 수 있어요.)

- 저는 아래처럼 잡고 시작할께요. (맨 밑의 긴 타임라인은 UISlider 로 만들었습니다.)

저는 기본적으로 버튼들로 만들었습니다.

2. 뷰에 동영상 집어넣기

- 영상은 방금 만드신 뷰 아래에 깔리도록 만들어야겠죠?

- 그전에 저는 앱에 미리 넣어둔 영상을 가져올꺼라 간단히 URL을 가져오는 함수부터 만들께요. 

func getSavedVideoUrl() -> URL {// 미리 저장된 영상 가져오기
    var savedViedoUrl = URL(fileURLWithPath: Bundle.main.path(forResource: "BigBuckBunny", ofType: "mp4")!)
    return savedViedoUrl
}
func getNetworkVideoUrl() -> URL {// 네트워크에서 영상 가져오기
    return URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!
}
// 본인이 영상을 가져오는 방법에 따라 선택해서 사용해주세요.

- 비디오 뷰를 천천히 만들어 봅시다. 

 

* 참고로 화면 회전을 만들 분들은 아래 함수를 넣어주세요. *

override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
    return .landscape // 화면이 자동 회전하도록
}

 

2-1 먼저 비디오를 넣기전 뷰를 만들어서 위치를 잡아주는 함수를 만듭니다.

- videoBackView 라고 이름을 지어줄께요.

- 저는 눈에 잘보이게 하기위해 바탕을 보라색으로 만들께요.

 let videoBackView = UIView() // 비디오가 재생되는 뷰
func setVideoAnotherView() {// 바탕 뷰 만들기
    videoBackView.backgroundColor = UIColor.purple // 보라색
    videoBackView.translatesAutoresizingMaskIntoConstraints = false
    let topConstraint = NSLayoutConstraint(item: videoBackView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0)
    let bottomConstraint = NSLayoutConstraint(item: videoBackView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)
    let leadingConstraint = NSLayoutConstraint(item: videoBackView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0)
    let trailingConstraint = NSLayoutConstraint(item: videoBackView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0)
    view.addSubview(videoBackView)
    view.sendSubviewToBack(videoBackView)
    view.addConstraints([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint])
}
override func viewDidLoad() {// 이안에 넣어주세요.
    super.viewDidLoad()
    self.setVideoAnotherView() // 뷰 생성
}

- 여기까지 하시면 아래처럼 보이게 됩니다. 

배경색은 원하시는 색으로 바꿔주세요.

 

2-2 그 후 해당 뷰에 비디오를 넣어주는 함수를 만듭니다. 

- moviePlayer 라고 이름을 지어줄께요.

var moviePlayer : AVPlayer? // 비디오 재생 장치
func setVideoView(url : URL){
    moviePlayer = AVPlayer(url: url)
    let playerLayer = AVPlayerLayer(player: moviePlayer)

    playerLayer.frame = videoBackView.frame;
    videoBackView.layer.addSublayer(playerLayer) // 아까 추가한 뷰에 넣어주기
    moviePlayer?.play() // 영상 재생
    // 나중에 이밑에 추가로 좀더 작성할껍니다. 
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.setVideoView(url: self.getSavedVideoUrl()) 
    // 전 이미 저장된 영상을 사용해 만들겁니다.
    //원하시는 URL 을 넣어주세요.
}

- 여기까지 하시면 영상이 재생되는게 보이실겁니다. 

잘 움직이시나요?

 

2-3 기본 창 닫기 (필요하신 분만 해주시면 됩니다.)

- 화면 우측 상단에 "X"모양이 보이시나요?

- 해당부분을 누르면 현재 창이 닫히도록 설정할겁니다. (이거 그냥 닫으시면 영상소리가 계속 들릴수있습니다!)

@IBAction func closeViewAction(_ sender: Any) {
    self.moviePlayer?.pause() //이거 안하고 닫으시면 소리계속납니다
    self.moviePlayer = nil 
    self.dismiss(animated: true)
}

 

3. 전체 버튼 UI 숨김 / 표시 기능 

- 클릭을 일정시간 안하면 자동으로 사라지거나 한번 더 누르면 사라지게 만드는게 좋겠어요. 

- 전체 UI를 가지고 있는 뷰와 일정시간이 지나는 걸 감지해주는 Timer를 선언하고 시작합시다.

@IBOutlet weak var totalBtnsView: UIView! // 전체 UIView 
var UItimer : Timer? // UI 사라지는 시간 측정

- 저희는 화면을 5초이상 안 누르거나 화면을 한번더 터치하면 UI가 안보이게 할껍니다.

//MARK: UI 타이머
func resetUITimer(){// 타이머 초기화
    UItimer?.invalidate()
    UItimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(hideUIControls), userInfo: nil, repeats: false)
}
@objc func hideUIControls(){ // UI 숨기기
    totalBtnsView.isHidden = true
}
@objc func toggleUIControls(){ // UI 보이거나 숨기게 하기
    totalBtnsView.isHidden = !totalBtnsView.isHidden
    self.resetUITimer()
}

- 이제 이걸 올바른 위치에 넣어줍시다. (아래 세줄 추가하시면 됩니다.)

 override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.setVideoView(url: self.getSavedVideoUrl()) 
	// 이 밑에서부터 추가로 넣어주세요.
    self.resetUITimer()
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.toggleUIControls))
    self.view.addGestureRecognizer(tapGesture)
}

- 여기까지 하시면 UI를 원하는대로 사라지거나 나타나게 할 수 있습니다. 

이제 창을 원하는대로 안보이게 할 수 있어요.

 

4. 영상 시간 표시 줄 제작 (Slider) + 원하는 부분으로 이동 

- 두 부분으로 나누어 제작할께요.

 

4-1 영상 시간 표시 줄 제작 (Slider) 

- 저 Slider 와 그 위에 Label 을 이용해서 타임 라인을 제작해 봅시다. 

- 위처럼 변수 정의 / 함수 정의 / 함수 적용 단계로 나누어 할텐데 함수 이름에 주의하세요. 

@IBOutlet weak var timeLineSlider: UISlider! // 시간 표시줄    
@IBOutlet weak var timeLineLbl: UILabel! // 00:00 / 00:00

var timeObserver : Any? // 동영상 시간 조절기

- 이제 시간이 흐를때마다 작동시킬 함수를 만들어 봅시다.

//MARK: 영상이 진행되는 동안 할일
func updateVideoPlayerSlider() {// 타임라인에 연결할 함수입니다.
    guard let currentTime = moviePlayer?.currentTime() else { return }
    let currentTimeInSeconds = CMTimeGetSeconds(currentTime)
    timeLineSlider.value = Float(currentTimeInSeconds)
    if let currentItem = moviePlayer?.currentItem {
        let duration = currentItem.duration
        if (CMTIME_IS_INVALID(duration)) {
            return;
        }
        let currentTime = currentItem.currentTime()
        timeLineSlider.value = Float(CMTimeGetSeconds(currentTime) / CMTimeGetSeconds(duration))
    }
    self.setTimeLabel() // 시간 표시하기
}
func setTimeLabel(){// 시간 표시하기
    if(getVideoTotalTime() != nil && getVideoCurrentTime() != nil){
        timeLineLbl.text = getVideoCurrentTime()! + " / " + getVideoTotalTime()!
    }else{
        timeLineLbl.text = "00:00 / 00:00"
    }
}
func getVideoTotalTime() -> String? {// 전체시간 가져오기
    let urlValue = self.getSavedVideoUrl()
    let asset: AVAsset = AVAsset(url: urlValue)
    let minute = Int(floor(asset.duration.seconds / 60))
    let second = Int(asset.duration.seconds) % 60
    return setTimeString(times: minute) + ":" + setTimeString(times: second)
}
func getVideoCurrentTime() -> String? {//현재 재생된 시간 가져오기
    guard let timeValue = moviePlayer?.currentItem?.currentTime() else {return nil}
    let minute = Int(floor(timeValue.seconds / 60))
    let second = Int(timeValue.seconds) % 60
    return setTimeString(times: minute) + ":" + setTimeString(times: second)
}
func setTimeString(times : Int) -> String {// 1 -> 01
    if(times < 10){
        return "0"+String(times)
    }else{
        return String(times)
    }
}

- 함수를 만들었으면 적용해야겠죠?

- 적용하시는 위치에 주의해주세요.

func setVideoView(url : URL){
        moviePlayer = AVPlayer(url: url)
        let playerLayer = AVPlayerLayer(player: moviePlayer)

        playerLayer.frame = videoBackView.frame;
        videoBackView.layer.addSublayer(playerLayer)
        moviePlayer?.play()
        // 아까 여기까지 작성하셨죠?
		//그밑에 아래 코드를 추가해주세요.
        let interval = CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        timeObserver = moviePlayer?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsedTime in
            self.updateVideoPlayerSlider()
        }) // 영상이 진행될때마다 특정 함수가 실행되는 코드 추가
    }

 

4-2 원하는 부분으로 이동 

- 만약 Slider 를 움직일 경우 원하는 부분으로 이동할수있어야겠죠? 

- 해당 부분은 영상을 가져오는 방식에따라 다소 렉이 걸릴 수 있습니다.

Slider 에 Value Changed 와 연결해주세요.

 //MARK: Slider 터치시
@IBAction func timeLineValueChanged(_ sender: Any) {
    moviePlayer?.pause() // 일단 정지
    guard let duration = moviePlayer?.currentItem?.duration else { return }
    let value = Float64(timeLineSlider.value) * CMTimeGetSeconds(duration)
    let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
        moviePlayer?.seek(to: seekTime, completionHandler: {okay in
            self.moviePlayer?.play() // 해당시간대로 이동이 가능하면 다시 플레이 시작
        })
    return
}

- 여기까지 하시게 되시면 아래처럼 보이게 됩니다. 

Slider 가 움직이시는게 보이나요?

 

5. 정지 / 재생 전환 버튼 제작

- 시작 전에 위에 해당 extention 을 추가해주세요.

extension AVPlayer{ // 영상이 현재 진행중인지 판단하는 부분
    var isPlaying: Bool {
           return rate != 0 && error == nil
       }
}

- 미리 만들어둔 버튼에 해당 기능을 넣으면 되겠죠?

//MARK: play 여부에 따른 버튼 이미지 변화
@IBAction func clickPlayAction(_ sender: Any) {
    if((self.moviePlayer?.isPlaying) ?? false){
        self.moviePlayer?.pause()
    }else{// 영상 정지시
        self.moviePlayer?.play()
    }
    self.setPlayBtnImage()
}
func setPlayBtnImage(){// 영상 플레이시
    if((self.moviePlayer?.isPlaying) ?? false){
        self.playBtn.setImage(UIImage(systemName: "pause.fill"), for: .normal)
    }else{// 영상 정지시
        self.playBtn.setImage(UIImage(systemName: "play.fill"), for: .normal)
    }
}

- 하시고 나서 지금까지 추가했던 부분에도 버튼이 자연스럽게 바뀌도록 만드는게 좋겠죠? 

- moviePlayer?.play() 아래에 밑의 코드처럼 한줄만 추가해주시면 자연스러워집니다. 

func setVideoView(url : URL){
        moviePlayer = AVPlayer(url: url)
        let playerLayer = AVPlayerLayer(player: moviePlayer)

        playerLayer.frame = videoBackView.frame;
        videoBackView.layer.addSublayer(playerLayer)
        moviePlayer?.play()
        setPlayBtnImage() // 버튼이 자연스럽게 바뀌도록 추가해주세요.

        let interval = CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        timeObserver = moviePlayer?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsedTime in
            self.updateVideoPlayerSlider()
        })
    }
//MARK: Slider 터치시
    @IBAction func timeLineValueChanged(_ sender: Any) {
        moviePlayer?.pause() // 일단 정지
        guard let duration = moviePlayer?.currentItem?.duration else { return }
        let value = Float64(timeLineSlider.value) * CMTimeGetSeconds(duration)
        let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
            moviePlayer?.seek(to: seekTime, completionHandler: {okay in
                self.moviePlayer?.play() // 해당시간대로 이동이 가능하면 다시 플레이 시작
                self.setPlayBtnImage() // 여기에 새로 추가해주세요
            })
        return
    }

- 여기까지 하시면 아래처럼 중앙 버튼을 통해 영상을 멈출 수 있습니다.

영상을 멈추는게 보이시나요?

 

6. 10초 전 / 후 버튼 제작

- 여기까지 하신분들은 어렵지 않게 하실수 있습니다.

- 마찬가지로 미리 만들어둔 버튼들에 함수를 연결해주세요. 

//MARK: 영상 10초 전후 이동   
@IBAction func goPlusVideoAction(_ sender: Any) {//10초 후로
    self.setActionMoveSeconds(goSecond: 10.0)
}
@IBAction func goMinusVideoAction(_ sender: Any) {//10초 전으로
    self.setActionMoveSeconds(goSecond: -10.0)
}

func setActionMoveSeconds(goSecond : Double){ // 원하는 초만큼 이동하는 부분
    self.moviePlayer?.pause()
    guard let currentTime = self.moviePlayer?.currentTime() else { return }
    let currentTimeInSecondsMove =  CMTimeGetSeconds(currentTime).advanced(by: goSecond) // 시간 계산 
    let seekTime = CMTime(value: CMTimeValue(currentTimeInSecondsMove), timescale: 1)
    self.moviePlayer?.seek(to: seekTime, completionHandler: {okay in
        self.moviePlayer?.play()
        self.setPlayBtnImage()
    })
}

 

7. 배속 재생 만들기 

- 저는 사용자가 누를때마다 0.5 배속 / 1배속 / 2배속 으로 바뀌도록 만들도록 할께요. 

 @IBOutlet weak var speedDisplayLbl: UILabel! // x n 배속
 var speedMode = 2 // 속도단계 표시기

- 스피드 모드 1단계(0.5배속) 2단계(1배속) 3단계(2배속) 로 나누어 만들겠습니다.

//MARK: 스피드 모드 제어
@IBAction func clickSpeedAction(_ sender: Any) {
    switch speedMode{
    case 1:
        self.setSpeedModeWithLevel(level: 2)
    case 2:
        self.setSpeedModeWithLevel(level: 3)
    case 3:
        self.setSpeedModeWithLevel(level: 1)
    default:
        break
    }
}

func setSpeedModeWithLevel(level: Int){
    switch level{
    case 1: // 0.5  배속
        speedMode = 1
        speedDisplayLbl.text = "x 0.5 배속"
        self.moviePlayer?.playImmediately(atRate: 0.5)
        break
    case 2: // 1 배속
        speedMode = 2
        speedDisplayLbl.text = "x 1.0 배속"
        self.moviePlayer?.playImmediately(atRate: 1.0)
        break
    case 3: // 2 배속
        speedMode = 3
        speedDisplayLbl.text = "x 2.0 배속"
        self.moviePlayer?.playImmediately(atRate: 2.0)
        break
    default:
        break
    }
}

- 다른 액션을 할때 속도가 원래대로 돌아오는 경우를 방지하기 위해 일부분에 코드를 추가하겠습니다. 

- self.moviePlayer?.play() 이 밑에 아래 코드를 하나씩 추가해주세요. 

self.setSpeedModeWithLevel(level: self.speedMode)// 속도 유지기

- 예를 들면 지금까지 한 코드에서 아래처럼 추가해주시면 됩니다.

@IBAction func timeLineValueChanged(_ sender: Any) {
        moviePlayer?.pause() // 일단 정지
        guard let duration = moviePlayer?.currentItem?.duration else { return }
        let value = Float64(timeLineSlider.value) * CMTimeGetSeconds(duration)
        let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
            moviePlayer?.seek(to: seekTime, completionHandler: {okay in
                self.moviePlayer?.play() // 해당시간대로 이동이 가능하면 다시 플레이 시작
                self.setPlayBtnImage()
                self.setSpeedModeWithLevel(level: self.speedMode)//여기 속도 추가해주세요
            })
        return
    }
 @IBAction func clickPlayAction(_ sender: Any) {
        if((self.moviePlayer?.isPlaying) ?? false){
            self.moviePlayer?.pause()
        }else{// 영상 정지시
            self.moviePlayer?.play()
            self.setSpeedModeWithLevel(level: self.speedMode)//여기 속도 추가해주세요
        }
        self.setPlayBtnImage()
    }
func setActionMoveSeconds(goSecond : Double){
    self.moviePlayer?.pause()
    guard let currentTime = self.moviePlayer?.currentTime() else { return }
    let currentTimeInSecondsMove =  CMTimeGetSeconds(currentTime).advanced(by: goSecond) // 시간 계산
    let seekTime = CMTime(value: CMTimeValue(currentTimeInSecondsMove), timescale: 1)
    self.moviePlayer?.seek(to: seekTime, completionHandler: {okay in
        self.moviePlayer?.play()
        self.setPlayBtnImage()
        self.setSpeedModeWithLevel(level: self.speedMode) //여기 속도 추가해주세요
    })
}

 

8. 다듬기 (어색한 부분 고치기 - 해도 되고 안해도 됩니다.)

- 시작할때 UI 일부가 어색해 보일수있으니 함수하나를 만들어 적용해줍니다. 

override func viewDidLoad() {
    super.viewDidLoad()
    self.setVideoAnotherView()
    self.setStartUIStatus() // 시작시 UI설정
}
func setStartUIStatus(){// 시작시 어색한 UI 미리 수정
    totalBtnsView.isHidden = true // 처음 시작시 안보이게
    timeLineLbl.text = "00:00 / 00:00"
    timeLineSlider.value = 0.0
    self.setSpeedModeWithLevel(level: 2) // 1배속 시작
}

이제 이렇게 완성하시면 아래처럼 영상을 재생할 수 있게 됩니다.

gif 라 그런지 엄청 버벅이네요;;

전체 코드는 다음 포스트에 올리도록 하겠습니다.

이 포스트가 누군가에게 도움이 되길 바라며

오늘도 파이팅입니다! 

 

p.s.

사실 이번 글은 한 분이 영어로 올리셨던 포스트를 참고해서 만들었는데요. 

원래는 해당 글 링크를 참고했다고 넣고 싶은데 

그 사이트를 다시 못찾아서 참조해서 넣지 못했어요;;;;

대부분의 코드는 그분의 포스트를 많이 참조하여 제작되었음을 알려드립니다. 

(나중에 사이트 찾으면 올려드릴께요.)

댓글