I am using an AVPlayer object to play a live stream of an MP3, just using an HTTP link.
As with normal live video playing in iOS, there is a button that you can press to skip to live.
Is it possible to do this with AVPlayer?
E.g I am listening live, pause, then after however long play again. It continues from where I left it. But what if I want to skip to live?
I had the same need and made this extension to AVPlayer:
extension AVPlayer {
func seekToLive() {
if let items = currentItem?.seekableTimeRanges, !items.isEmpty {
let range = items[items.count - 1]
let timeRange = range.timeRangeValue
let startSeconds = CMTimeGetSeconds(timeRange.start)
let durationSeconds = CMTimeGetSeconds(timeRange.duration)
seek(to: CMTimeMakeWithSeconds(startSeconds + durationSeconds, 1))
}
}
}
Related
I am building a karaoke app with the ability to sing with video so here is my problem:
I am recording the user video (video only from the front camera) along with applying voice filters with audiokit on a separate audio records.
Now in my playback, i want to play the video and the audio in a sync mode but it didn't succeed because a have an out sync of video and audio.
I am using akplayer for audio so i can apply voice mod and vlckit for playing user video.
do {
//MARK: VLC kit part of the video setup
Vlc_VideoPlayer = VLCMediaPlayer()
Vlc_VideoPlayer.media = VLCMedia(url: recordVideoURL)
Vlc_VideoPlayer.addObserver(self, forKeyPath: "time", options: [], context: nil)
Vlc_VideoPlayer.addObserver(self, forKeyPath: "remainingTime", options: [], context: nil)
Vlc_VideoPlayer.drawable = self.CameraView
//MARK: Audiokit with AKPlayer Setup
file = try AKAudioFile(forReading: recordVoiceURL)
player = AKPlayer(audioFile: file)
self.player.preroll()
delay = AKVariableDelay(player)
delay.rampTime = 0.5
delayMixer = AKDryWetMixer(player, delay)
reverb = AKCostelloReverb(delayMixer)
reverbMixer = AKDryWetMixer(delayMixer, reverb)
booster = AKBooster(reverbMixer)
tracker = AKAmplitudeTracker(booster)
AudioKit.output = tracker
try AudioKit.start()
}catch{
print (error)
}
self.startPlayers()
now the startPlayers function :
func startPlayers(){
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if AudioKit.engine.isRunning {
self.Vlc_VideoPlayer.audio.isMuted = true
self.Vlc_VideoPlayer.play()
self.player.isLooping = false
self.player.play()
}else{
self.startPlayers()
}
}
}
I don't know anything about the VLC player, but with the built in AVPlayer there is an option to sync to a clock:
var time: TimeInterval = 1 // 1 second in the future
videoPlayer.masterClock = CMClockGetHostTimeClock()
let hostTime = mach_absolute_time()
let cmHostTime = CMClockMakeHostTimeFromSystemUnits(hostTime)
let cmVTime = CMTimeMakeWithSeconds(time, preferredTimescale: videoPlayer.currentTime().timescale)
let futureTime = CMTimeAdd(cmHostTime, cmVTime)
videoPlayer.setRate(1, time: CMTime.invalid, atHostTime: futureTime)
AKPlayer then supports syncing to the mach_absolute_time() hostTime using its scheduling functions. As you have above, the two will start close together but there is no guarantee of any sync.
Trying to start two players will work out of pure look and unless you have means to synchronize playback after it started, it will not be perfect. Ideally, you should play the audio with VLC as well to make use of its internal synchronization tools.
To iterate on what you have right now, I would suggest to start playback with VLC until it decoded the first frame, pause, start your audio and continue playback with VLC as soon as you decoded the first audio sample. This will still not be perfect, but probably better.
All of my other audio files play perfectly through the audio player. Most of the files are less than 5 seconds long, and the longest file that still plays is 23 seconds long. For some reason my 1:15 long file "sortSong" is completely silent when it is called.
Here is the code for my audio player:
import Foundation
import AVFoundation
class AudioPlayer {
static let sharedInstance = AudioPlayer()
enum Sound: String {
case sortSongPlay = "sortSong"
case centerButtonRelease = "centerButtonRelease"
case buttonTap = "tapSound"
static var all: [Sound] {
return [.centerButtonRelease, .buttonTap, sortSongPlay]
}
var url: URL {
return URL.init(fileURLWithPath: Bundle.main.path(forResource: self.rawValue, ofType: "mp3")!)
}
}
private var soundMap = [String: SystemSoundID]()
init() {
for sound in Sound.all {
let soundUrl = sound.url
var soundId: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundUrl as CFURL, &soundId);
soundMap[sound.rawValue] = soundId
}
}
func play(sound: Sound) {
AudioServicesPlaySystemSound(soundMap[sound.rawValue]!)
}
}
The sound is played when this function is called in my view controller.
func successfulSort() {
AudioPlayer.sharedInstance.play(sound: .sortSongPlay)
rotateSortImageOne()
rotateSortImageTwo()
}
This is the action that calls the successfulSort func
if inputText == "hello" && seedArray == [1, 4, 1] {
successfulSort()
}
If I simply change the case sortSongPlay to = "shortSortSong" (the 23 second version) it plays just fine.
All of my sound files have their target memberships checked for this project file, and all of the files have the correct path. The audio will play in the interface builder if I press the play button. There are no compiler errors or warnings, the app never crashes, the audio for sortSong simply isn't playing when it is called in the app.
This is a link containing examples I have tried on my project. The first two sounds play silently in the app while the shorter sounds all play perfectly. https://soundcloud.com/spiffystache
What is causing this to be silent?
You should put a correct title on your question.
Your code does not use AVAudioPlayer, but uses System Sound Services.
The documentation clearly states its limitation:
You can use System Sound Services to play short (30 seconds or
shorter) sounds.
I have never checked if its limitation is exactly 30 seconds, but it matches the description in your question.
Consider using AVAudioPlayer (as in the title) to play longer sound.
I need to create a Audio Player for streamed URL (m3u8 format). I have created music player using AVPlayer. But I need to show visualizer for streamed song. I have tried different solution but not found any working example of it.
I have created visualizer using AVAudioPlayer(averagePower) but it won't support streamed URL.
Any help to show visualizer for AVPlayer? Thanks in advance.
I have also tried using MYAudioTapProcessor which most of the people suggested, but for streamed URL, tracks always returns null.
Added the MYAudioTapProcessor.h and MYAudioTapProcessor.m in project
//Initialization of player
let playerItem = AVPlayerItem( url:NSURL( string:"https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8" ) as! URL )
let audioPlayer: AVPlayer = AVPlayer(playerItem:playerItem)
//Added periodic time observer
audioPlayer!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if audioPlayer!.currentItem?.status == .readyToPlay
{
if let playerItem: AVPlayerItem = audioPlayer!.currentItem {
print(playerItem.asset.tracks.count)
if (playerItem.asset.tracks) != nil {
self.tapProcessor = MYAudioTapProcessor(avPlayerItem: playerItem)
playerItem.audioMix = self.tapProcessor.audioMix
self.tapProcessor.delegate = self
}
}
}
}
//Delegate callback method for MYAudioTapProcessor
func audioTabProcessor(_ audioTabProcessor: MYAudioTapProcessor!, hasNewLeftChannelValue leftChannelValue: Float, rightChannelValue: Float) {
print("volume: \(leftChannelValue) : \(rightChannelValue)")
volumeSlider.value = leftChannelValue
}
Also tried by adding the "Track" observer.
playerItem.addObserver(self, forKeyPath: "tracks", options: NSKeyValueObservingOptions.new, context: nil);
Now if play mp3 file, the callback method calls but for m3u8 callback method doesn't call. The main reason for failing m3u8 URL is it always show tracks array count zero whereas for mp3 files tracks array has one item.
You cannot get tracks for HLS via AVPLayer. You should use progressive download or local file for getting audio tracks while playing media.
I'm making an iOS app in Swift that plays a video in a loop in a small layer in the top right corner of the screen which shows a video of specific coloured item. the user then taps the corresponding coloured item on the screen. when they do, the videoName variable is randomly changed to the next colour and the corresponding video is triggered. I have no problem raising, playing and looping the video with AVPlayer, AVPlayerItem as seen in the attached code.
Where I'm stuck is that whenever the next video is shown, previous ones stay open behind it. Also, After 16 videos have played, the player disappears altogether on my iPad. I've tried many suggestions presented in this and other sites but either swift finds a problem with them or it just doesn't work.
So question: within my code here, how do I tell it "hey the next video has started to play, remove the previous video and it's layer and free up the memory so i can play as many videos as needed"?
//set variables for video play
var playerItem:AVPlayerItem?
var player:AVPlayer?
//variables that contain video file path, name and extension
var videoPath = NSBundle.mainBundle().resourcePath!
var videoName = "blue"
let videoExtension = ".mp4"
//DISPLAY VIDEO
func showVideo(){
//Assign url path
let url = NSURL(fileURLWithPath: videoPath+"/Base.lproj/"+videoName+videoExtension)
playerItem = AVPlayerItem(URL: url)
player=AVPlayer(playerItem: playerItem!)
let playerLayer=AVPlayerLayer(player: player!)
//setplayser location in uiview and show video
playerLayer.frame=CGRectMake(700, 5, 350, 350)
self.view.layer.addSublayer(playerLayer)
player!.play()
// Add notification to know when the video ends, then replay it again. THIS IS A CONTINUAL LOOP
NSNotificationCenter.defaultCenter().addObserverForName(AVPlayerItemDidPlayToEndTimeNotification, object: player!.currentItem, queue: nil)
{ notification in
let t1 = CMTimeMake(5, 100);
self.player!.seekToTime(t1)
self.player!.play()
}
}
`
I adapted #Anupam Mishra's Swift code suggestion. It wasn't working at first but finally figured I had to take the playerLayer outside the function and close the playerLayer after I set the player to nil. Also. instead of using 'if(player!.rate>0 ...)' which would no doubt still work, I created a variable switch to indicate when to say "kill the player AND the layer" as seen below. It may not be pretty but it works! The following is for absolute newbies like myself - WHAT I LEARNED FROM THIS EXPERIENCE: it seems to me that an ios device only allows 16 layers to be added to a viewController (or superLayer). so each layer needs to be deleted before opening the next layer with its player unless you really want 16 layers running all at once. WHAT THIS CODE BELOW ACTUALLY DOES FOR YOU: this code creates a re-sizable layer over an existing viewController and plays a video from your bundle in an endless loop. When the next video is about to be called, the current video and the layer are totally removed, freeing up the memory, and then a new layer with the new video is played. The video layer size and location is totally customizable using the playerLayer.frame=CGRectMake(left, top, width, height) parameters. HOW TO MAKE IT ALL WORK: Assuming you've already added your videos to you bundle, create another function for your button tap. in that function, first call the 'stopPlayer()' function, change the 'videoName' variable to the new video name you desire, then call the 'showVideo()' function. (if you need to change the video extension, change 'videoExtension' from a let to a var.
`
//set variables for video play
var playerItem:AVPlayerItem?
var player:AVPlayer?
var playerLayer = AVPlayerLayer() //NEW playerLayer var location
//variables that contain video file path, name and extension
var videoPath = NSBundle.mainBundle().resourcePath!
var videoName = "blue"
let videoExtension = ".mp4"
var createLayerSwitch = true /*NEW switch to say whether on not to create the layer when referenced by the closePlayer and showVideo functions*/
//DISPLAY VIDEO
func showVideo(){
//Assign url path
let url = NSURL(fileURLWithPath: videoPath+"/Base.lproj/"+videoName+videoExtension)
playerItem = AVPlayerItem(URL: url)
player=AVPlayer(playerItem: playerItem!)
playerLayer=AVPlayerLayer(player: player!) //NEW: remove 'let' from playeLayer here.
//setplayser location in uiview and show video
playerLayer.frame=CGRectMake(700, 5, 350, 350)
self.view.layer.addSublayer(playerLayer)
player!.play()
createLayerSwitch = false //NEW switch to tell if a layer is already created or not. I set the switch to false so that when the next tapped item/button references the closePlayer() function, the condition is triggered to close the player AND the layer
// Add notification to know when the video ends, then replay it again without a pause between replays. THIS IS A CONTINUAL LOOP
NSNotificationCenter.defaultCenter().addObserverForName(AVPlayerItemDidPlayToEndTimeNotification, object: player!.currentItem, queue: nil)
{ notification in
let t1 = CMTimeMake(5, 100);
self.player!.seekToTime(t1)
self.player!.play()
}
}
//NEW function to kill the current player and layer before playing the next video
func closePlayer(){
if (createLayerSwitch == false) {
player!.pause()
player = nil
playerLayer.removefromsuperlayer()
}
}
`
Why don't just use replaceCurrentItemWithPlayerItem on your player ? You will keep the same player for all your videos. I think it's a better way to do.
Edit : replaceCurrentItemWithPlayerItem has to be call on the main thread
Before moving on next track first check, is player having any videos or music, to stop it do the following checks:
Swift Code-
if player!.rate > 0 && player!.error == nil
{
player!.pause()
player = nil
}
Objective-C Code-
if (player.rate > 0 && !player.error)
{
[player setRate:0.0];
}
I am trying to play a video using MPMoviePlayerController for an iOS app in Swift.
My goal is to be able to play system music with something like apple music, then open my app and have the audio mix in, but I want my app to be able to take control of MPNowPlayingInfoCenter.
How can I use AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: .MixWithOthers) while still set the MPNowPlayingInfoCenter?
Google Maps mixes in audio while taking setting MPNowPlayingInfoCenter. Below is how I am trying to set the MPNowPlayingInfoCenter:
func setMeta(){
UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
self.becomeFirstResponder()
if let player = PlayWorkoutViewController.player{
let coverArt = MPMediaItemArtwork(image: UIImage(named: "AlbumArt")!)
let dict: [String: AnyObject] = [
MPMediaItemPropertyArtwork: coverArt,
MPMediaItemPropertyTitle:workout.title,
MPMediaItemPropertyArtist:"Alex",
MPMediaItemPropertyAlbumTitle:workout.program.title,
MPNowPlayingInfoPropertyPlaybackRate: player.currentPlaybackRate,
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentPlaybackTime,
MPMediaItemPropertyPlaybackDuration: player.playableDuration
]
MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = dict
}
}
The above function works when I am not trying to play outside music with an option (.MixWithOthers) at the same time, but while I am trying to play outside music with the option (.MixWithOthers) the info center does not update.
Edit 1: Just to make things super clear, I already having video playing properly I am trying to play video with other background audio while being able to set MPNowPlayingInfoCenter.
This isn't currently possible in iOS. Even just changing your category options to .MixWithOthers causes your nowPlayingInfo to be ignored.
My guess is iOS only considers non-mixing apps for inclusion in MPNowPlayingInfoCenter, because there is uncertainty as to which app would show up in (e.g.) Control Center if there are multiple mixing apps playing at the same time.
I'd very much like it if iOS used a best-effort approach for choosing the "now playing app", something like this:
If there's a non-mixing app playing, pick that. Else..
If there's only one mixing app playing, pick that. Else..
If there are multiple mixing apps playing, just pick one :) Or pick none, I'm fine with either.
If you'd like this behavior as well, I'd encourage you to file a bug with Apple.
Have you tried implementing your own custom function to update the MPNowPlayingInfoCenter? Recently I was using an AVAudioPlayer to play music and needed to do the updating manually.
This is basically the function I called upon a new song being loaded.
func updateNowPlayingCenter() {
let center = MPNowPlayingInfoCenter.defaultCenter()
if nowPlayingItem == nil {
center.nowPlayingInfo = nil
} else {
var songInfo = [String: AnyObject]()
// Add item to dictionary if it exists
if let artist = nowPlayingItem?.artist {
songInfo[MPMediaItemPropertyArtist] = artist
}
if let title = nowPlayingItem?.title {
songInfo[MPMediaItemPropertyTitle] = title
}
if let albumTitle = nowPlayingItem?.albumTitle {
songInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
}
if let playbackDuration = nowPlayingItem?.playbackDuration {
songInfo[MPMediaItemPropertyPlaybackDuration] = playbackDuration
}
if let artwork = nowPlayingItem?.artwork {
songInfo[MPMediaItemPropertyArtwork] = artwork
}
center.nowPlayingInfo = songInfo
}
}
I am not sure if doing this upon a movie being loaded will override the MPMoviePlayerController, but it seems worth a shot.
Additionally, they have depreciated MPMoviePlayerController and replaced it with AVPlayerViewController, so thats also worth looking into.
Edit: Also I would check to make sure that you are properly receiving remote control events, as this impacts the data being displayed by the info center.
To play the video in swift use this:-
func playVideoEffect() {
let path = NSBundle.mainBundle().pathForResource("egg_grabberAnmi", ofType:"mp4")
let url = NSURL.fileURLWithPath(path!)
self.moviePlayer = MPMoviePlayerController(contentURL: url)
if let player = moviePlayer {
let screenSize: CGRect = UIScreen.mainScreen().bounds
player.view.frame = CGRect(x: frame.size.width*0.10,y: frame.size.width/2, width:screenSize.width * 0.80, height: screenSize.width * 0.80)
player.view.sizeToFit()
player.scalingMode = MPMovieScalingMode.Fill
player.fullscreen = true
player.controlStyle = MPMovieControlStyle.None
player.movieSourceType = MPMovieSourceType.File
player.play()
self.view?.addSubview(player.view)
var timer = NSTimer.scheduledTimerWithTimeInterval(6.0, target: self, selector: Selector("update"), userInfo: nil, repeats: false)
}
}
// And Use the function to play Video
playVideoEffect()