Video Streaming Tutorial for iOS: Getting Started

Learn how to build a video streaming app using AVKit and AVFoundation frameworks. By Saeed Taheri.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Implementing the Actual Looping

Apple wrote a nifty new class called AVPlayerLooper. This class will take a single-player item and take care of all the logic it takes to play that item on a loop. Unfortunately, that doesn’t help you here!

Annoyed emoji-like face

What you want is to play all of these videos on a loop. Looks like you’ll have to do things the manual way. All you need to do is keep track of your player and the currently playing item. When it gets to the last video, you’ll add all the clips to the queue again.

When it comes to “keeping track” of a player’s information, the only route you have is to use key-value observing (KVO).

Frustrated emoji-like face

Yeah, it’s one of the wonkier APIs Apple has come up with. If you’re careful, it’s a powerful way to observe and respond to state changes in real time. If you’re completely unfamiliar with KVO, here’s the quick explanation: The basic idea is that you register for notification any time the value of a particular property changes. In this case, you want to know whenever player‘s currentItem changes. Each time you’re notified, you’ll know the player has advanced to the next video.

To use KVO in Swift — much nicer than in Objective-C — you need to retain a reference to the observer. Add the following property to the existing properties in LoopingPlayerUIView:

private var token: NSKeyValueObservation?

To start observing the property, add the following to the end of init(urls:):

token = player?.observe(\.currentItem) { [weak self] player, _ in
  if player.items().count == 1 {
    self?.addAllVideosToPlayer()
  }
}

Here, you’re registering a block to run each time the player’s currentItem property changes. When the current video changes, you want to check to see if the player has moved to the final video. If it has, then it’s time to add all the video clips back to the queue.

That’s all there is to it! Build and run to see your clips looping indefinitely.

Properly looping clip in simulator

Playing with Player Controls

Next, it’s time to add some controls. Your tasks are to:

  1. Unmute the video when a single-tap occurs.
  2. Toggle between 1x and 2x speed when a double-tap occurs.

You’ll start with the actual methods you need to accomplish these things. First, you need to expose some methods in LoopingPlayerUIView where you have access to player directly. Second, you need to create a way to call those methods from LoopingPlayerView.

Add these methods to LoopingPlayerUIView:

func setVolume(_ value: Float) {
  player?.volume = value
}

func setRate(_ value: Float) {
  player?.rate = value
}

As the names imply, you can use these methods to control video volume and playback rate. You can also pass 0.0 to setRate(_:) to pause the video.

The way you can connect these methods to SwiftUI is by using a Binding.

Add these properties to LoopingPlayerView right under let videoURLs: [URL]:

@Binding var rate: Float
@Binding var volume: Float

Make sure to pass the binding values to the underlying UIView using the methods you already implemented:

func makeUIView(context: Context) -> LoopingPlayerUIView {
  let view = LoopingPlayerUIView(urls: videoURLs)
  
  view.setVolume(volume)
  view.setRate(rate)
  
  return view
}

func updateUIView(_ uiView: LoopingPlayerUIView, context: Context) {
  uiView.setVolume(volume)
  uiView.setRate(rate)
}

This time, you also add some lines to updateUIView(_:context:) to account for changes in volume and rate while the view is on screen.

Since you’ll control the playback from outside this struct, you can remove these two lines from the initializer of LoopingPlayerUIView:

player?.volume = 0.0
player?.play()

Now, head back to VideoFeedView.swift and add these state properties which you use to change and observe embedded video’s volume and playback rate:

@State private var embeddedVideoRate: Float = 0.0
@State private var embeddedVideoVolume: Float = 0.0

Then, pass the following state properties to LoopingPlayerView in makeEmbeddedVideoPlayer():

LoopingPlayerView(
  videoURLs: videoClips,
  rate: $embeddedVideoRate,
  volume: $embeddedVideoVolume)

Finally, add the following view modifiers to LoopingPlayerView in makeEmbeddedVideoPlayer():

// 1
.onAppear {
  embeddedVideoRate = 1
}

// 2
.onTapGesture(count: 2) {
  embeddedVideoRate = embeddedVideoRate == 1.0 ? 2.0 : 1.0
}

// 3
.onTapGesture {
  embeddedVideoVolume = embeddedVideoVolume == 1.0 ? 0.0 : 1.0
}

Taking it comment by comment:

  1. By setting the rate to 1.0, you make the video play, just like before.
  2. You add a listener for when someone double-taps the player view. This toggles between playback rate of 2x and 1x.
  3. You add a listener for when someone single-taps the player view. This toggles the mute status of video.
Note: Make sure to add the double-tap listener first, then the single-tap. If you do it in reverse, the double-tap listener will never get called.

Build and run again, and you’ll be able to tap and double-tap to play around with the speed and volume of the clips. This shows how easy it is to add custom controls for interfacing with a custom video view.

Custom video view

Now, you can pump up the volume and throw things into overdrive at the tap of a finger. Pretty neat!

Playing Video Efficiently

One thing to note before moving on is that playing video is a resource-intensive task. As things are, your app will continue to play these clips, even when you start watching a full-screen video.

To fix this issue, go to VideoFeedView.swift and find the onAppear block of VideoPlayer in makeFullScreenVideoPlayer(for:). Stop the video clip playback by setting the rate to 0.0:

embeddedVideoRate = 0.0

To resume the playback when the full-screen video closes, find the fullScreenCover view modifier in the body of VideoFeedView and add the following after the On Dismiss Closure comment:

embeddedVideoRate = 1.0

You can also stop playing videos and remove all items from the player object when it’s no longer needed by the system. To do so, return to LoopingPlayerView.swift and add this method to LoopingPlayerUIView:

func cleanup() {
  player?.pause()
  player?.removeAllItems()
  player = nil
}

Fortunately, SwiftUI provides a way to call this cleanup method. Add the following to LoopingPlayerView:

static func dismantleUIView(_ uiView: LoopingPlayerUIView, coordinator: ()) {
  uiView.cleanup()
}

This makes your wrapper a very good citizen in the SwiftUI world!

Build and run, and go to a full-screen video. The preview will resume where it left off when you return to the feed.

Video clip that pauses when full screen video plays