All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.bjoernpetersen.jmusicbot.playback.Player.kt Maven / Gradle / Ivy

There is a newer version: 0.25.0
Show newest version
package com.github.bjoernpetersen.jmusicbot.playback

import com.github.bjoernpetersen.jmusicbot.Loggable
import com.github.bjoernpetersen.jmusicbot.Song
import com.github.bjoernpetersen.jmusicbot.playback.PlaybackStateListener.PlaybackState
import com.github.bjoernpetersen.jmusicbot.provider.BrokenSuggesterException
import com.github.bjoernpetersen.jmusicbot.provider.Suggester
import com.google.common.util.concurrent.ThreadFactoryBuilder
import java.io.Closeable
import java.io.IOException
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import java.util.logging.Logger
import kotlin.concurrent.withLock

class Player(private val songPlayedNotifier: Consumer, private val suggester: Suggester?) : Loggable,
    Closeable {

  private val logger: Logger = createLogger()
  private val autoPlayer: ExecutorService = Executors.newSingleThreadExecutor(
      ThreadFactoryBuilder()
          .setDaemon(true)
          .setNameFormat("playerPool-%d")
          .build()
  )

  /**
   * This player's queue.
   */
  val queue: Queue = Queue()

  private val stateLock: Lock = ReentrantLock()
  /**
   * The current state of this player. This might be play, pause, stop or error.
   */
  var state: PlayerState = StopState()
    private set(value) {
      field = value
      for (listener in stateListeners) {
        listener.onChanged(value)
      }
    }
  private var playback: Playback = DummyPlayback

  private val stateListeners: MutableSet = HashSet()

  init {
    autoPlayer.submit { this.autoPlay() }
    queue.addListener(object : QueueChangeListener {
      override fun onAdd(entry: QueueEntry) {
        entry.song.load()
      }

      override fun onRemove(entry: QueueEntry) {}

      override fun onMove(entry: QueueEntry, fromIndex: Int, toIndex: Int) {}
    })

    if (suggester != null) {
      addListener(PlayerStateListener { _ -> preloadSuggestion(suggester) })
    }
  }

  private fun preloadSuggestion(suggester: Suggester) {
    if (queue.isEmpty) {
      val suggestions: List
      try {
        suggestions = suggester.getNextSuggestions(1)
      } catch (e: BrokenSuggesterException) {
        return
      }

      suggestions[0].load()
    }
  }

  override fun getLogger(): Logger {
    return logger
  }

  /**
   * Adds a [PlayerStateListener] which will be called everytime the [state] changes.
   */
  fun addListener(listener: PlayerStateListener) {
    stateListeners.add(listener)
  }

  /**
   * Removes a [PlayerStateListener] previously registered with [addListener].
   */
  fun removeListener(listener: PlayerStateListener) {
    stateListeners.remove(listener)
  }

  /**
   * Pauses the player.
   *
   * If the player is not currently playing anything, nothing will be done.
   *
   * This method blocks until the playback is paused.
   */
  @Throws(InterruptedException::class)
  fun pause() {
    stateLock.withLock {
      logFinest("Pausing...")
      val state = state
      if (state is PauseState) {
        logFinest("Already paused.")
        return
      } else if (state !is PlayState) {
        logFiner("Tried to pause player in state %s", state)
        return
      }
      playback.pause()
      this.state = state.pause()
    }
  }

  /**
   * Resumes the playback.
   *
   * If the player is not currently pausing, nothing will be done.
   *
   * This method blocks until the playback is resumed.
   */
  @Throws(InterruptedException::class)
  fun play() {
    stateLock.withLock {
      logFinest("Playing...")
      val state = state
      if (state is PlayState) {
        logFinest("Already playing.")
        return
      } else if (state !is PauseState) {
        logFiner("Tried to play in state %s", state)
        return
      }
      playback.play()
      this.state = state.play()
    }
  }

  /**
   * Plays the next song.
   *
   * This method will play the next song from the queue.
   * If the queue is empty, the next suggested song from the primary [suggester] will be used.
   * If there is no primary suggester, the player will transition into the [StopState].
   *
   * This method blocks until either a new song is playing or the StopState is reached.
   */
  @Throws(InterruptedException::class)
  fun next() {
    val state = this.state
    stateLock.withLock {
      logFiner("Next...")
      val newState = this.state
      if (isSignificantlyDifferent(state, newState)) {
        logFinest("Skipping next call due to state change while waiting for lock.")
        return
      }

      try {
        playback.close()
      } catch (e: Exception) {
        logWarning(e, "Error closing playback")
      }

      val nextOptional = queue.pop()
      if (!nextOptional.isPresent && suggester == null) {
        logFinest("Queue is empty. Stopping.")
        playback = DummyPlayback
        this.state = StopState()
        return
      }

      val nextEntry: SongEntry = if (nextOptional.isPresent) {
        nextOptional.get()
      } else try {
        SuggestedSongEntry(suggester!!.suggestNext())
      } catch (e: BrokenSuggesterException) {
        logFine("Default suggester could not suggest anything. Stopping.")
        playback = DummyPlayback
        this.state = StopState()
        return
      }

      val nextSong = nextEntry.song
      songPlayedNotifier.accept(nextEntry)
      logFine("Next song is: " + nextSong)
      try {
        playback = nextSong.playback
      } catch (e: IOException) {
        logWarning(e, "Error creating playback")

        this.state = ErrorState()
        playback = DummyPlayback
        return
      }

      playback.setPlaybackStateListener { this.onPlaybackFeedback(it) }

      this.state = PauseState(nextEntry)
      play()
    }
  }

  private fun onPlaybackFeedback(feedback: PlaybackState) {
    logFinest("Playback state update: %s", feedback)
    stateLock.withLock {
      val state = state
      when (feedback) {
        PlaybackStateListener.PlaybackState.PLAY -> if (state is PauseState) {
          this.state = state.play()
        }
        PlaybackStateListener.PlaybackState.PAUSE -> if (state is PlayState) {
          this.state = state.pause()
        }
      }
    }
  }

  private fun isSignificantlyDifferent(state: PlayerState, other: PlayerState): Boolean {
    val stateSong = state.entry
    val otherSong = other.entry
    // TODO check for correctness
    return ((otherSong == null) != (stateSong == null)) || ((otherSong != null) && (stateSong != otherSong))
  }

  private fun autoPlay() {
    try {
      val state = this.state
      logFinest("Waiting for song to finish")
      playback.waitForFinish()
      logFinest("Waiting done")

      stateLock.withLock {
        // Prevent auto next calls if next was manually called
        if (isSignificantlyDifferent(this.state, state)) {
          logFinest("Skipping auto call to next()")
        } else {
          logFinest("Auto call to next()")
          next()
        }
      }

      autoPlayer.submit { this.autoPlay() }
    } catch (e: InterruptedException) {
      logFine("autoPlay interrupted", e)
    }
  }

  @Throws(IOException::class)
  override fun close() {
    try {
      autoPlayer.shutdownNow()
      playback.close()
    } catch (e: Exception) {
      throw IOException(e)
    }
  }
}

private object DummyPlayback : Playback {
  override fun play() {}

  override fun pause() {}

  @Throws(InterruptedException::class)
  override fun waitForFinish() {
    Thread.sleep(2000)
  }

  override fun close() {}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy