Merge pull request #563 from biconou/squash

Introduction of a new kind of jukebox player based on the javasound api.
master
Rémi 7 years ago committed by GitHub
commit f5454bb0fd
  1. 21
      airsonic-main/pom.xml
  2. 181
      airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueService.java
  3. 18
      airsonic-main/src/main/java/org/airsonic/player/command/PlayerSettingsCommand.java
  4. 12
      airsonic-main/src/main/java/org/airsonic/player/controller/PlayQueueController.java
  5. 6
      airsonic-main/src/main/java/org/airsonic/player/controller/PlayerSettingsController.java
  6. 80
      airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java
  7. 15
      airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java
  8. 18
      airsonic-main/src/main/java/org/airsonic/player/domain/Player.java
  9. 7
      airsonic-main/src/main/java/org/airsonic/player/domain/PlayerTechnology.java
  10. 356
      airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java
  11. 210
      airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java
  12. 238
      airsonic-main/src/main/java/org/airsonic/player/service/JukeboxService.java
  13. 4
      airsonic-main/src/main/java/org/airsonic/player/service/PlayerService.java
  14. 17
      airsonic-main/src/main/resources/liquibase/6.3/add-player-mixer.xml
  15. 1
      airsonic-main/src/main/resources/liquibase/6.3/changelog.xml
  16. 3
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties
  17. 35
      airsonic-main/src/main/webapp/WEB-INF/jsp/javaJukeboxPlayerControlBar.jspf
  18. 25
      airsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp
  19. 58
      airsonic-main/src/main/webapp/WEB-INF/jsp/playerSettings.jsp
  20. 7
      airsonic-main/src/main/webapp/script/moment-2.18.1.min.js
  21. 505
      airsonic-main/src/main/webapp/script/moment-with-locales-2.18.1.min.js
  22. 102
      airsonic-main/src/main/webapp/script/playQueue/javaJukeboxPlayerControlBar.js
  23. 16
      airsonic-main/src/main/webapp/style/default-without-mediaelement-light.css

@ -31,6 +31,25 @@
<version>${project.version}</version>
</dependency>
<!-- Java Audio Player and needed dependencies
-->
<dependency>
<groupId>com.github.biconou</groupId>
<artifactId>AudioPlayer</artifactId>
<version>0.2.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- -->
<!-- Metrics
Metrics is a cool framework used here
to compute musures and statistics during automated testing
@ -317,7 +336,7 @@
<version>${cxf.version}</version>
<exclusions>
<exclusion>
<groupId> org.springframework</groupId>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
</exclusions>

@ -89,27 +89,22 @@ public class PlayQueueService {
}
public PlayQueueInfo start() throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
return doStart(request, response);
}
public PlayQueueInfo doStart(HttpServletRequest request, HttpServletResponse response) throws Exception {
Player player = getCurrentPlayer(request, response);
Player player = resolvePlayer();
player.getPlayQueue().setStatus(PlayQueue.Status.PLAYING);
return convert(request, player, true);
if (player.isJukebox()) {
jukeboxService.start(player);
}
public PlayQueueInfo stop() throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
return doStop(request, response);
return convert(resolveHttpServletRequest(), player, true);
}
public PlayQueueInfo doStop(HttpServletRequest request, HttpServletResponse response) throws Exception {
Player player = getCurrentPlayer(request, response);
public PlayQueueInfo stop() throws Exception {
Player player = resolvePlayer();
player.getPlayQueue().setStatus(PlayQueue.Status.STOPPED);
return convert(request, player, true);
if (player.isJukebox()) {
jukeboxService.stop(player);
}
return convert(resolveHttpServletRequest(), player, true);
}
public PlayQueueInfo toggleStartStop() throws Exception {
@ -129,16 +124,17 @@ public class PlayQueueService {
}
public PlayQueueInfo skip(int index) throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
return doSkip(request, response, index, 0);
return doSkip(index, 0);
}
public PlayQueueInfo doSkip(HttpServletRequest request, HttpServletResponse response, int index, int offset) throws Exception {
Player player = getCurrentPlayer(request, response);
public PlayQueueInfo doSkip(int index, int offset) throws Exception {
Player player = resolvePlayer();
player.getPlayQueue().setIndex(index);
boolean serverSidePlaylist = !player.isExternalWithPlaylist();
return convert(request, player, serverSidePlaylist, offset);
if (serverSidePlaylist && player.isJukebox()) {
jukeboxService.skip(player,index,offset);
}
return convert(resolveHttpServletRequest(), player, serverSidePlaylist, offset);
}
public PlayQueueInfo reloadSearchCriteria() throws Exception {
@ -202,23 +198,24 @@ public class PlayQueueService {
boolean serverSidePlaylist = !player.isExternalWithPlaylist();
if (serverSidePlaylist && currentIndex != -1) {
doSkip(request, response, currentIndex, (int) (positionMillis / 1000L));
doSkip(currentIndex, (int) (positionMillis / 1000L));
}
return result;
}
public PlayQueueInfo play(int id) throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
HttpServletRequest request = resolveHttpServletRequest();
HttpServletResponse response = resolveHttpServletResponse();
Player player = getCurrentPlayer(request, response);
MediaFile file = mediaFileService.getMediaFile(id);
List<MediaFile> songs;
if (file.isFile()) {
String username = securityService.getCurrentUsername(request);
boolean queueFollowingSongs = settingsService.getUserSettings(username).isQueueFollowingSongs();
List<MediaFile> songs;
if (queueFollowingSongs) {
MediaFile dir = mediaFileService.getParentOf(file);
songs = mediaFileService.getChildrenOf(dir, true, false, true);
@ -229,11 +226,10 @@ public class PlayQueueService {
} else {
songs = Arrays.asList(file);
}
return doPlay(request, player, songs).setStartPlayerAt(0);
} else {
List<MediaFile> songs = mediaFileService.getDescendantsOf(file, true);
return doPlay(request, player, songs).setStartPlayerAt(0);
songs = mediaFileService.getDescendantsOf(file, true);
}
return doPlay(request, player, songs).setStartPlayerAt(0);
}
/**
@ -422,6 +418,9 @@ public class PlayQueueService {
}
player.getPlayQueue().addFiles(false, files);
player.getPlayQueue().setRandomSearchCriteria(null);
if (player.isJukebox()) {
jukeboxService.play(player);
}
return convert(request, player, true);
}
@ -461,69 +460,81 @@ public class PlayQueueService {
return doAdd(request, response, new int[]{id}, index);
}
public PlayQueueInfo doAdd(HttpServletRequest request, HttpServletResponse response, int[] ids, Integer index) throws Exception {
Player player = getCurrentPlayer(request, response);
/**
* TODO This method should be moved to a real PlayQueueService not dedicated to Ajax DWR.
* @param playQueue
* @param ids
* @param index
* @return
* @throws Exception
*/
public PlayQueue addMediaFilesToPlayQueue(PlayQueue playQueue,int[] ids, Integer index, boolean removeVideoFiles) throws Exception {
List<MediaFile> files = new ArrayList<MediaFile>(ids.length);
for (int id : ids) {
MediaFile ancestor = mediaFileService.getMediaFile(id);
files.addAll(mediaFileService.getDescendantsOf(ancestor, true));
}
if (player.isWeb()) {
if (removeVideoFiles) {
mediaFileService.removeVideoFiles(files);
}
if (index != null) {
player.getPlayQueue().addFilesAt(files, index);
playQueue.addFilesAt(files, index);
} else {
player.getPlayQueue().addFiles(true, files);
playQueue.addFiles(true, files);
}
player.getPlayQueue().setRandomSearchCriteria(null);
return convert(request, player, false);
playQueue.setRandomSearchCriteria(null);
return playQueue;
}
public PlayQueueInfo doSet(HttpServletRequest request, HttpServletResponse response, int[] ids) throws Exception {
public PlayQueueInfo doAdd(HttpServletRequest request, HttpServletResponse response, int[] ids, Integer index) throws Exception {
Player player = getCurrentPlayer(request, response);
PlayQueue playQueue = player.getPlayQueue();
boolean removeVideoFiles = false;
if (player.isWeb()) {
removeVideoFiles = true;
}
addMediaFilesToPlayQueue(player.getPlayQueue(), ids, index, removeVideoFiles);
return convert(request, player, false);
}
/**
* TODO This method should be moved to a real PlayQueueService not dedicated to Ajax DWR.
* @param playQueue
* @param ids
* @return
* @throws Exception
*/
public PlayQueue resetPlayQueue(PlayQueue playQueue,int[] ids, boolean removeVideoFiles) throws Exception {
MediaFile currentFile = playQueue.getCurrentFile();
PlayQueue.Status status = playQueue.getStatus();
playQueue.clear();
PlayQueueInfo result = doAdd(request, response, ids, null);
addMediaFilesToPlayQueue(playQueue, ids, null,removeVideoFiles);
int index = currentFile == null ? -1 : playQueue.getFiles().indexOf(currentFile);
playQueue.setIndex(index);
playQueue.setStatus(status);
return result;
return playQueue;
}
public PlayQueueInfo clear() throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
return doClear(request, response);
}
public PlayQueueInfo doClear(HttpServletRequest request, HttpServletResponse response) throws Exception {
Player player = getCurrentPlayer(request, response);
Player player = resolvePlayer();
player.getPlayQueue().clear();
boolean serverSidePlaylist = !player.isExternalWithPlaylist();
return convert(request, player, serverSidePlaylist);
return convert(resolveHttpServletRequest(), player, serverSidePlaylist);
}
public PlayQueueInfo shuffle() throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
return doShuffle(request, response);
}
public PlayQueueInfo doShuffle(HttpServletRequest request, HttpServletResponse response) throws Exception {
Player player = getCurrentPlayer(request, response);
public PlayQueueInfo shuffle() throws Exception {
Player player = resolvePlayer();
player.getPlayQueue().shuffle();
return convert(request, player, false);
return convert(resolveHttpServletRequest(), player, false);
}
public PlayQueueInfo remove(int index) throws Exception {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
return doRemove(request, response, index);
Player player = resolvePlayer();
player.getPlayQueue().removeFileAt(index);
return convert(resolveHttpServletRequest(), player, false);
}
public PlayQueueInfo toggleStar(int index) throws Exception {
@ -629,10 +640,6 @@ public class PlayQueueService {
return convert(request, player, false);
}
public void setGain(float gain) {
jukeboxService.setGain(gain);
}
private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean serverSidePlaylist) throws Exception {
return convert(request, player, serverSidePlaylist, 0);
}
@ -640,9 +647,9 @@ public class PlayQueueService {
private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean serverSidePlaylist, int offset) throws Exception {
String url = NetworkService.getBaseUrl(request);
if (serverSidePlaylist && player.isJukebox()) {
jukeboxService.updateJukebox(player, offset);
}
/* if (serverSidePlaylist && player.isJukebox()) {
updateJukebox(player, offset);
} */
boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr());
boolean m3uSupported = player.isExternal() || player.isExternalWithPlaylist();
@ -671,7 +678,10 @@ public class PlayQueueService {
coverArtUrl, remoteCoverArtUrl));
}
boolean isStopEnabled = playQueue.getStatus() == PlayQueue.Status.PLAYING && !player.isExternalWithPlaylist();
float gain = jukeboxService.getGain();
float gain = 0.0f;
gain = jukeboxService.getGain(player);
return new PlayQueueInfo(entries, isStopEnabled, playQueue.isRepeatEnabled(), playQueue.isRadioEnabled(), serverSidePlaylist, gain);
}
@ -704,6 +714,43 @@ public class PlayQueueService {
return playerService.getPlayer(request, response);
}
private Player resolvePlayer() {
return getCurrentPlayer(resolveHttpServletRequest(), resolveHttpServletResponse());
}
private HttpServletRequest resolveHttpServletRequest() {
return WebContextFactory.get().getHttpServletRequest();
}
private HttpServletResponse resolveHttpServletResponse() {
return WebContextFactory.get().getHttpServletResponse();
}
//
// Methods dedicated to jukebox
//
public void setGain(float gain) {
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
Player player = getCurrentPlayer(request, response);
if (player != null) {
jukeboxService.setGain(player,gain);
}
}
public void setJukeboxPosition(int positionInSeconds) {
Player player = resolvePlayer();
jukeboxService.setPosition(player,positionInSeconds);
}
//
// End : Methods dedicated to jukebox
//
public void setPlayerService(PlayerService playerService) {
this.playerService = playerService;
}

@ -52,6 +52,8 @@ public class PlayerSettingsCommand {
private EnumHolder[] transcodeSchemeHolders;
private Player[] players;
private boolean isAdmin;
private String javaJukeboxMixer;
private String[] javaJukeboxMixers;
public String getPlayerId() {
return playerId;
@ -209,6 +211,22 @@ public class PlayerSettingsCommand {
public void setReloadNeeded(boolean reloadNeeded) {
}
public String getJavaJukeboxMixer() {
return javaJukeboxMixer;
}
public void setJavaJukeboxMixer(String javaJukeboxMixer) {
this.javaJukeboxMixer = javaJukeboxMixer;
}
public String[] getJavaJukeboxMixers() {
return javaJukeboxMixers;
}
public void setJavaJukeboxMixers(String[] javaJukeboxMixers) {
this.javaJukeboxMixers = javaJukeboxMixers;
}
/**
* Holds the transcoding and whether it is active for the given player.
*/

@ -70,16 +70,4 @@ public class PlayQueueController {
map.put("autoHide", userSettings.isAutoHidePlayQueue());
return new ModelAndView("playQueue","model",map);
}
public void setPlayerService(PlayerService playerService) {
this.playerService = playerService;
}
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
}

@ -19,6 +19,7 @@
*/
package org.airsonic.player.controller;
import com.github.biconou.AudioPlayer.AudioSystemUtils;
import org.airsonic.player.command.PlayerSettingsCommand;
import org.airsonic.player.domain.*;
import org.airsonic.player.service.PlayerService;
@ -36,6 +37,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@ -102,6 +104,9 @@ public class PlayerSettingsController {
command.setPlayers(players.toArray(new Player[players.size()]));
command.setAdmin(user.isAdminRole());
command.setJavaJukeboxMixers(Arrays.stream(AudioSystemUtils.listAllMixers()).map(info -> info.getName()).toArray(String[]::new));
command.setJavaJukeboxMixer(player.getJavaJukeboxMixer());
model.addAttribute("command",command);
}
@ -115,6 +120,7 @@ public class PlayerSettingsController {
player.setName(StringUtils.trimToNull(command.getName()));
player.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName()));
player.setTechnology(PlayerTechnology.valueOf(command.getTechnologyName()));
player.setJavaJukeboxMixer(command.getJavaJukeboxMixer());
playerService.updatePlayer(player);
transcodingService.setTranscodingsForPlayer(player, command.getActiveTranscodingIds());

@ -29,6 +29,7 @@ import org.airsonic.player.dao.MediaFileDao;
import org.airsonic.player.dao.PlayQueueDao;
import org.airsonic.player.domain.*;
import org.airsonic.player.domain.Bookmark;
import org.airsonic.player.domain.PlayQueue;
import org.airsonic.player.service.*;
import org.airsonic.player.util.Pair;
import org.airsonic.player.util.StringUtil;
@ -833,51 +834,70 @@ public class SubsonicRESTController {
return;
}
Player player = playerService.getPlayer(request, response);
boolean returnPlaylist = false;
String action = getRequiredStringParameter(request, "action");
if ("start".equals(action)) {
playQueueService.doStart(request, response);
} else if ("stop".equals(action)) {
playQueueService.doStop(request, response);
} else if ("skip".equals(action)) {
switch (action) {
case "start":
player.getPlayQueue().setStatus(PlayQueue.Status.PLAYING);
jukeboxService.start(player);
break;
case "stop":
player.getPlayQueue().setStatus(PlayQueue.Status.STOPPED);
jukeboxService.stop(player);
break;
case "skip":
int index = getRequiredIntParameter(request, "index");
int offset = getIntParameter(request, "offset", 0);
playQueueService.doSkip(request, response, index, offset);
} else if ("add".equals(action)) {
int[] ids = getIntParameters(request, "id");
playQueueService.doAdd(request, response, ids, null);
} else if ("set".equals(action)) {
player.getPlayQueue().setIndex(index);
jukeboxService.skip(player,index,offset);
break;
case "add":
int[] ids = getIntParameters(request, "id");
playQueueService.doSet(request, response, ids);
} else if ("clear".equals(action)) {
playQueueService.doClear(request, response);
} else if ("remove".equals(action)) {
int index = getRequiredIntParameter(request, "index");
playQueueService.doRemove(request, response, index);
} else if ("shuffle".equals(action)) {
playQueueService.doShuffle(request, response);
} else if ("setGain".equals(action)) {
playQueueService.addMediaFilesToPlayQueue(player.getPlayQueue(),ids,null,true);
break;
case "set":
ids = getIntParameters(request, "id");
playQueueService.resetPlayQueue(player.getPlayQueue(),ids,true);
break;
case "clear":
player.getPlayQueue().clear();
break;
case "remove":
index = getRequiredIntParameter(request, "index");
player.getPlayQueue().removeFileAt(index);
break;
case "shuffle":
player.getPlayQueue().shuffle();
break;
case "setGain":
float gain = getRequiredFloatParameter(request, "gain");
jukeboxService.setGain(gain);
} else if ("get".equals(action)) {
jukeboxService.setGain(player,gain);
break;
case "get":
returnPlaylist = true;
} else if ("status".equals(action)) {
break;
case "status":
// No action necessary.
} else {
break;
default:
throw new Exception("Unknown jukebox action: '" + action + "'.");
}
Player player = playerService.getPlayer(request, response);
String username = securityService.getCurrentUsername(request);
Player jukeboxPlayer = jukeboxService.getPlayer();
boolean controlsJukebox = jukeboxPlayer != null && jukeboxPlayer.getId().equals(player.getId());
org.airsonic.player.domain.PlayQueue playQueue = player.getPlayQueue();
PlayQueue playQueue = player.getPlayQueue();
// this variable is only needed for the JukeboxLegacySubsonicService. To be removed.
boolean controlsJukebox = jukeboxService.canControl(player);
int currentIndex = controlsJukebox && !playQueue.isEmpty() ? playQueue.getIndex() : -1;
boolean playing = controlsJukebox && !playQueue.isEmpty() && playQueue.getStatus() == org.airsonic.player.domain.PlayQueue.Status.PLAYING;
float gain = jukeboxService.getGain();
int position = controlsJukebox && !playQueue.isEmpty() ? jukeboxService.getPosition() : 0;
boolean playing = controlsJukebox && !playQueue.isEmpty() && playQueue.getStatus() == PlayQueue.Status.PLAYING;
float gain;
int position;
gain = jukeboxService.getGain(player);
position = controlsJukebox && !playQueue.isEmpty() ? jukeboxService.getPosition(player) : 0;
Response res = createResponse();
if (returnPlaylist) {

@ -20,6 +20,7 @@
package org.airsonic.player.dao;
import org.airsonic.player.domain.*;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.RowMapper;
@ -40,7 +41,7 @@ public class PlayerDao extends AbstractDao {
private static final Logger LOG = LoggerFactory.getLogger(PlayerDao.class);
private static final String INSERT_COLUMNS = "name, type, username, ip_address, auto_control_enabled, m3u_bom_enabled, " +
"last_seen, cover_art_scheme, transcode_scheme, dynamic_ip, technology, client_id";
"last_seen, cover_art_scheme, transcode_scheme, dynamic_ip, technology, client_id, mixer";
private static final String QUERY_COLUMNS = "id, " + INSERT_COLUMNS;
private PlayerRowMapper rowMapper = new PlayerRowMapper();
@ -81,9 +82,13 @@ public class PlayerDao extends AbstractDao {
* @return The player with the given ID, or <code>null</code> if no such player exists.
*/
public Player getPlayerById(String id) {
if (StringUtils.isBlank(id)) {
return null;
} else {
String sql = "select " + QUERY_COLUMNS + " from player where id=?";
return queryOne(sql, rowMapper, id);
}
}
/**
* Creates a new player.
@ -103,7 +108,7 @@ public class PlayerDao extends AbstractDao {
player.getIpAddress(), player.isAutoControlEnabled(), player.isM3uBomEnabled(),
player.getLastSeen(), CoverArtScheme.MEDIUM.name(),
player.getTranscodeScheme().name(), player.isDynamicIp(),
player.getTechnology().name(), player.getClientId());
player.getTechnology().name(), player.getClientId(), player.getJavaJukeboxMixer());
addPlaylist(player);
LOG.info("Created player " + id + '.');
@ -154,12 +159,13 @@ public class PlayerDao extends AbstractDao {
"transcode_scheme = ?, " +
"dynamic_ip = ?, " +
"technology = ?, " +
"client_id = ? " +
"client_id = ?, " +
"mixer = ? " +
"where id = ?";
update(sql, player.getName(), player.getType(), player.getUsername(),
player.getIpAddress(), player.isAutoControlEnabled(), player.isM3uBomEnabled(),
player.getLastSeen(), player.getTranscodeScheme().name(), player.isDynamicIp(),
player.getTechnology().name(), player.getClientId(), player.getId());
player.getTechnology().name(), player.getClientId(), player.getJavaJukeboxMixer(), player.getId());
}
private void addPlaylist(Player player) {
@ -188,6 +194,7 @@ public class PlayerDao extends AbstractDao {
player.setDynamicIp(rs.getBoolean(col++));
player.setTechnology(PlayerTechnology.valueOf(rs.getString(col++)));
player.setClientId(rs.getString(col++));
player.setJavaJukeboxMixer(rs.getString(col++));
addPlaylist(player);
return player;

@ -44,6 +44,7 @@ public class Player {
private Date lastSeen;
private TranscodeScheme transcodeScheme = TranscodeScheme.OFF;
private PlayQueue playQueue;
private String javaJukeboxMixer;
/**
* Returns the player ID.
@ -120,7 +121,11 @@ public class Player {
}
public boolean isJukebox() {
return technology == PlayerTechnology.JUKEBOX;
return (technology == PlayerTechnology.JUKEBOX || technology == PlayerTechnology.JAVA_JUKEBOX);
}
public boolean isJavaJukebox() {
return (technology == PlayerTechnology.JAVA_JUKEBOX);
}
public boolean isExternal() {
@ -326,6 +331,16 @@ public class Player {
return "Player " + id;
}
public void setJavaJukeboxMixer(String javaJukeboxMixer) {
this.javaJukeboxMixer = javaJukeboxMixer;
}
public String getJavaJukeboxMixer() {
return javaJukeboxMixer;
}
/**
* Returns a string representation of the player.
*
@ -336,4 +351,5 @@ public class Player {
public String toString() {
return getDescription();
}
}

@ -45,6 +45,11 @@ public enum PlayerTechnology {
/**
* Plays music directly on the audio device of the Airsonic server.
*/
JUKEBOX
JUKEBOX,
/**
* Jukebox player that uses the Java Sound API.
*/
JAVA_JUKEBOX
}

@ -0,0 +1,356 @@
package org.airsonic.player.service;
import com.github.biconou.AudioPlayer.JavaPlayer;
import com.github.biconou.AudioPlayer.api.*;
import org.airsonic.player.domain.*;
import org.airsonic.player.domain.Player;
import org.airsonic.player.util.FileUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
/**
* @author R??mi Cocula
*/
@Service
public class JukeboxJavaService {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(JukeboxJavaService.class);
@Autowired
private AudioScrobblerService audioScrobblerService;
@Autowired
private StatusService statusService;
@Autowired
private SettingsService settingsService;
@Autowired
private SecurityService securityService;
@Autowired
private MediaFileService mediaFileService;
private TransferStatus status;
private Map<String, com.github.biconou.AudioPlayer.api.Player> activeAudioPlayers = new Hashtable<>();
private Map<String, List<com.github.biconou.AudioPlayer.api.Player>> activeAudioPlayersPerMixer = new Hashtable<>();
private final static String DEFAULT_MIXER_ENTRY_KEY = "_default";
/**
* Finds the corresponding active audio player for a given airsonic player.
* The JukeboxJavaService references all active audio players in a map indexed by airsonic player id.
*
* @param airsonicPlayer a given airsonic player.
* @return the corresponding active audio player of null if none exists.
*/
private com.github.biconou.AudioPlayer.api.Player retrieveAudioPlayerForAirsonicPlayer(Player airsonicPlayer) {
com.github.biconou.AudioPlayer.api.Player foundPlayer = activeAudioPlayers.get(airsonicPlayer.getId());
if (foundPlayer == null) {
synchronized (activeAudioPlayers) {
foundPlayer = initAudioPlayer(airsonicPlayer);
if (foundPlayer == null) {
throw new RuntimeException("Did not initialized a player");
} else {
activeAudioPlayers.put(airsonicPlayer.getId(), foundPlayer);
String mixer = airsonicPlayer.getJavaJukeboxMixer();
if (StringUtils.isBlank(mixer)) {
mixer = DEFAULT_MIXER_ENTRY_KEY;
}
List<com.github.biconou.AudioPlayer.api.Player> playersForMixer = activeAudioPlayersPerMixer.get(mixer);
if (playersForMixer == null) {
playersForMixer = new ArrayList<>();
activeAudioPlayersPerMixer.put(mixer, playersForMixer);
}
playersForMixer.add(foundPlayer);
}
}
}
return foundPlayer;
}
private com.github.biconou.AudioPlayer.api.Player initAudioPlayer(final Player airsonicPlayer) {
if (!airsonicPlayer.getTechnology().equals(PlayerTechnology.JAVA_JUKEBOX)) {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player");
}
log.info("begin initAudioPlayer");
com.github.biconou.AudioPlayer.api.Player audioPlayer;
if (StringUtils.isNotBlank(airsonicPlayer.getJavaJukeboxMixer())) {
log.info("use mixer : {}", airsonicPlayer.getJavaJukeboxMixer());
audioPlayer = new JavaPlayer(airsonicPlayer.getJavaJukeboxMixer());
} else {
log.info("use default mixer");
audioPlayer = new JavaPlayer();
}
if (audioPlayer != null) {
audioPlayer.registerListener(new PlayerListener() {
@Override
public void onBegin(int index, File currentFile) {
onSongStart(airsonicPlayer);
}
@Override
public void onEnd(int index, File file) {
onSongEnd(airsonicPlayer);
}
@Override
public void onFinished() {
airsonicPlayer.getPlayQueue().setStatus(PlayQueue.Status.STOPPED);
}
@Override
public void onStop() {
airsonicPlayer.getPlayQueue().setStatus(PlayQueue.Status.STOPPED);
}
@Override
public void onPause() {
// Nothing to do here
}
});
log.info("New audio player {} has been initialized.", audioPlayer.toString());
} else {
throw new RuntimeException("AudioPlayer has not been initialized properly");
}
return audioPlayer;
}
public int getPosition(final Player airsonicPlayer) {
if (!airsonicPlayer.getTechnology().equals(PlayerTechnology.JAVA_JUKEBOX)) {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player");
}
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
if (audioPlayer == null) {
return 0;
} else {
return audioPlayer.getPlayingInfos().currentAudioPositionInSeconds();
}
}
public void setPosition(final Player airsonicPlayer, int positionInSeconds) {
if (!airsonicPlayer.getTechnology().equals(PlayerTechnology.JAVA_JUKEBOX)) {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player");
}
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
if (audioPlayer != null) {
audioPlayer.setPos(positionInSeconds);
} else {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " has no real audio player");
}
}
public float getGain(final Player airsonicPlayer) {
if (!airsonicPlayer.getTechnology().equals(PlayerTechnology.JAVA_JUKEBOX)) {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player");
}
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
if (audioPlayer != null) {
return audioPlayer.getGain();
}
return 0.5f;
}
public void setGain(final Player airsonicPlayer, final float gain) {
if (!airsonicPlayer.getTechnology().equals(PlayerTechnology.JAVA_JUKEBOX)) {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player");
}
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
log.debug("setGain : gain={}", gain);
if (audioPlayer != null) {
audioPlayer.setGain(gain);
} else {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " has no real audio player");
}
}
private void onSongStart(Player player) {
MediaFile file = player.getPlayQueue().getCurrentFile();
log.info("[onSongStart] {} starting jukebox for \"{}\"", player.getUsername(), FileUtil.getShortPath(file.getFile()));
if (status != null) {
statusService.removeStreamStatus(status);
status = null;
}
status = statusService.createStreamStatus(player);
status.setFile(file.getFile());
status.addBytesTransfered(file.getFileSize());
mediaFileService.incrementPlayCount(file);
scrobble(player, file, false);
}
private void onSongEnd(Player player) {
MediaFile file = player.getPlayQueue().getCurrentFile();
log.info("[onSongEnd] {} stopping jukebox for \"{}\"", player.getUsername(), FileUtil.getShortPath(file.getFile()));
if (status != null) {
statusService.removeStreamStatus(status);
status = null;
}
scrobble(player, file, true);
}
private void scrobble(Player player, MediaFile file, boolean submission) {
if (player.getClientId() == null) { // Don't scrobble REST players.
audioScrobblerService.register(file, player.getUsername(), submission, null);
}
}
/**
* Plays the playqueue of a jukebox player starting at the beginning.
*
* @param airsonicPlayer
*/
public void play(Player airsonicPlayer) {
log.debug("begin play jukebox : player = id:{};name:{}", airsonicPlayer.getId(), airsonicPlayer.getName());
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
// Control user authorizations
User user = securityService.getUserByName(airsonicPlayer.getUsername());
if (!user.isJukeboxRole()) {
log.warn("{} is not authorized for jukebox playback.", user.getUsername());
return;
}
log.debug("Different file to play -> start a new play list");
if (airsonicPlayer.getPlayQueue().getCurrentFile() != null) {
audioPlayer.setPlayList(new PlayList() {
@Override
public File getNextAudioFile() throws IOException {
airsonicPlayer.getPlayQueue().next();
return getCurrentAudioFile();
}
@Override
public File getCurrentAudioFile() {
MediaFile current = airsonicPlayer.getPlayQueue().getCurrentFile();
if (current != null) {
return airsonicPlayer.getPlayQueue().getCurrentFile().getFile();
} else {
return null;
}
}
@Override
public int getSize() {
return airsonicPlayer.getPlayQueue().size();
}
@Override
public int getIndex() {
return airsonicPlayer.getPlayQueue().getIndex();
}
});
// Close any other player using the same mixer.
String mixer = airsonicPlayer.getJavaJukeboxMixer();
if (StringUtils.isBlank(mixer)) {
mixer = DEFAULT_MIXER_ENTRY_KEY;
}
List<com.github.biconou.AudioPlayer.api.Player> playersForSameMixer = activeAudioPlayersPerMixer.get(mixer);
playersForSameMixer.forEach(player -> {
if (player != audioPlayer) {
player.close();
}
});
audioPlayer.play();
}
}
public void start(Player airsonicPlayer) throws Exception {
log.debug("begin start jukebox : player = id:{};name:{}", airsonicPlayer.getId(), airsonicPlayer.getName());
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
// Control user authorizations
User user = securityService.getUserByName(airsonicPlayer.getUsername());
if (!user.isJukeboxRole()) {
log.warn("{} is not authorized for jukebox playback.", user.getUsername());
return;
}
log.debug("PlayQueue.Status is {}", airsonicPlayer.getPlayQueue().getStatus());
audioPlayer.play();
}
public void stop(Player airsonicPlayer) throws Exception {
log.debug("begin stop jukebox : player = id:{};name:{}", airsonicPlayer.getId(), airsonicPlayer.getName());
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
// Control user authorizations
User user = securityService.getUserByName(airsonicPlayer.getUsername());
if (!user.isJukeboxRole()) {
log.warn("{} is not authorized for jukebox playback.", user.getUsername());
return;
}
log.debug("PlayQueue.Status is {}", airsonicPlayer.getPlayQueue().getStatus());
audioPlayer.pause();
}
/**
* @param airsonicPlayer
* @param index
* @throws Exception
*/
public void skip(Player airsonicPlayer, int index, int offset) throws Exception {
log.debug("begin skip jukebox : player = id:{};name:{}", airsonicPlayer.getId(), airsonicPlayer.getName());
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
// Control user authorizations
User user = securityService.getUserByName(airsonicPlayer.getUsername());
if (!user.isJukeboxRole()) {
log.warn("{} is not authorized for jukebox playback.", user.getUsername());
return;
}
if (index == 0 && offset == 0) {
play(airsonicPlayer);
} else {
if (offset == 0) {
audioPlayer.stop();
audioPlayer.play();
} else {
audioPlayer.setPos(offset);
}
}
}
public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
this.audioScrobblerService = audioScrobblerService;
}
public void setStatusService(StatusService statusService) {
this.statusService = statusService;
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
public void setMediaFileService(MediaFileService mediaFileService) {
this.mediaFileService = mediaFileService;
}
}

@ -0,0 +1,210 @@
/*
This file is part of Airsonic.
Airsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Airsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2016 (C) Airsonic Authors
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
*/
package org.airsonic.player.service;
import org.airsonic.player.domain.*;
import org.airsonic.player.service.jukebox.AudioPlayer;
import org.airsonic.player.util.FileUtil;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.InputStream;
/**
* Plays music on the local audio device.
*
* @author Sindre Mehus
*/
@Service
public class JukeboxLegacySubsonicService implements AudioPlayer.Listener {
private static final Logger LOG = LoggerFactory.getLogger(JukeboxLegacySubsonicService.class);
@Autowired
private TranscodingService transcodingService;
@Autowired
private AudioScrobblerService audioScrobblerService;
@Autowired
private StatusService statusService;
@Autowired
private SettingsService settingsService;
@Autowired
private SecurityService securityService;
@Autowired
private MediaFileService mediaFileService;
private AudioPlayer audioPlayer;
private Player player;
private TransferStatus status;
private MediaFile currentPlayingFile;
private float gain = AudioPlayer.DEFAULT_GAIN;
private int offset;
/**
* Updates the jukebox by starting or pausing playback on the local audio device.
*
* @param player The player in question.
* @param offset Start playing after this many seconds into the track.
*/
public synchronized void updateJukebox(Player player, int offset) throws Exception {
User user = securityService.getUserByName(player.getUsername());
if (!user.isJukeboxRole()) {
LOG.warn(user.getUsername() + " is not authorized for jukebox playback.");
return;
}
if (player.getPlayQueue().getStatus() == PlayQueue.Status.PLAYING) {
this.player = player;
MediaFile result;
synchronized (player.getPlayQueue()) {
result = player.getPlayQueue().getCurrentFile();
}
play(result, offset);
} else {
if (audioPlayer != null) {
audioPlayer.pause();
}
}
}
private synchronized void play(MediaFile file, int offset) {
InputStream in = null;
try {
// Resume if possible.
boolean sameFile = file != null && file.equals(currentPlayingFile);
boolean paused = audioPlayer != null && audioPlayer.getState() == AudioPlayer.State.PAUSED;
if (sameFile && paused && offset == 0) {
audioPlayer.play();
} else {
this.offset = offset;
if (audioPlayer != null) {
audioPlayer.close();
if (currentPlayingFile != null) {
onSongEnd(currentPlayingFile);
}
}
if (file != null) {
int duration = file.getDurationSeconds() == null ? 0 : file.getDurationSeconds() - offset;
TranscodingService.Parameters parameters = new TranscodingService.Parameters(file, new VideoTranscodingSettings(0, 0, offset, duration, false));
String command = settingsService.getJukeboxCommand();
parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false));
in = transcodingService.getTranscodedInputStream(parameters);
audioPlayer = new AudioPlayer(in, this);
audioPlayer.setGain(gain);
audioPlayer.play();
onSongStart(file);
}
}
currentPlayingFile = file;
} catch (Exception x) {
LOG.error("Error in jukebox: " + x, x);
IOUtils.closeQuietly(in);
}
}
public synchronized void stateChanged(AudioPlayer audioPlayer, AudioPlayer.State state) {
if (state == AudioPlayer.State.EOM) {
player.getPlayQueue().next();
MediaFile result;
synchronized (player.getPlayQueue()) {
result = player.getPlayQueue().getCurrentFile();
}
play(result, 0);
}
}
public synchronized float getGain() {
return gain;
}
public synchronized int getPosition() {
return audioPlayer == null ? 0 : offset + audioPlayer.getPosition();
}
/**
* Returns the player which currently uses the jukebox.
*
* @return The player, may be {@code null}.
*/
public Player getPlayer() {
return player;
}
private void onSongStart(MediaFile file) {
LOG.info(player.getUsername() + " starting jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\"");
status = statusService.createStreamStatus(player);
status.setFile(file.getFile());
status.addBytesTransfered(file.getFileSize());
mediaFileService.incrementPlayCount(file);
scrobble(file, false);
}
private void onSongEnd(MediaFile file) {
LOG.info(player.getUsername() + " stopping jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\"");
if (status != null) {
statusService.removeStreamStatus(status);
}
scrobble(file, true);
}
private void scrobble(MediaFile file, boolean submission) {
if (player.getClientId() == null) { // Don't scrobble REST players.
audioScrobblerService.register(file, player.getUsername(), submission, null);
}
}
public synchronized void setGain(float gain) {
this.gain = gain;
if (audioPlayer != null) {
audioPlayer.setGain(gain);
}
}
public void setTranscodingService(TranscodingService transcodingService) {
this.transcodingService = transcodingService;
}
public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
this.audioScrobblerService = audioScrobblerService;
}
public void setStatusService(StatusService statusService) {
this.statusService = statusService;
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
public void setMediaFileService(MediaFileService mediaFileService) {
this.mediaFileService = mediaFileService;
}
}

@ -20,191 +20,163 @@
package org.airsonic.player.service;
import org.airsonic.player.domain.*;
import org.airsonic.player.service.jukebox.AudioPlayer;
import org.airsonic.player.util.FileUtil;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.InputStream;
/**
* Plays music on the local audio device.
*
* @author Sindre Mehus
*
* @author R?mi Cocula
*/
@Service
public class JukeboxService implements AudioPlayer.Listener {
public class JukeboxService {
private static final Logger LOG = LoggerFactory.getLogger(JukeboxService.class);
private static final Logger log = LoggerFactory.getLogger(JukeboxService.class);
private AudioPlayer audioPlayer;
@Autowired
private TranscodingService transcodingService;
@Autowired
private AudioScrobblerService audioScrobblerService;
@Autowired
private StatusService statusService;
@Autowired
private SettingsService settingsService;
private JukeboxLegacySubsonicService jukeboxLegacySubsonicService;
@Autowired
private SecurityService securityService;
private JukeboxJavaService jukeboxJavaService;
private Player player;
private TransferStatus status;
private MediaFile currentPlayingFile;
private float gain = AudioPlayer.DEFAULT_GAIN;
private int offset;
@Autowired
private MediaFileService mediaFileService;
/**
* Updates the jukebox by starting or pausing playback on the local audio device.
*
* @param player The player in question.
* @param offset Start playing after this many seconds into the track.
*/
public synchronized void updateJukebox(Player player, int offset) throws Exception {
User user = securityService.getUserByName(player.getUsername());
if (!user.isJukeboxRole()) {
LOG.warn(user.getUsername() + " is not authorized for jukebox playback.");
return;
}
if (player.getPlayQueue().getStatus() == PlayQueue.Status.PLAYING) {
this.player = player;
MediaFile result;
synchronized (player.getPlayQueue()) {
result = player.getPlayQueue().getCurrentFile();
}
play(result, offset);
} else {
if (audioPlayer != null) {
audioPlayer.pause();
public void setGain(Player airsonicPlayer,float gain) {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
jukeboxLegacySubsonicService.setGain(gain);
break;
case JAVA_JUKEBOX:
jukeboxJavaService.setGain(airsonicPlayer,gain);
break;
}
}
}
private synchronized void play(MediaFile file, int offset) {
InputStream in = null;
try {
// Resume if possible.
boolean sameFile = file != null && file.equals(currentPlayingFile);
boolean paused = audioPlayer != null && audioPlayer.getState() == AudioPlayer.State.PAUSED;
if (sameFile && paused && offset == 0) {
audioPlayer.play();
} else {
this.offset = offset;
if (audioPlayer != null) {
audioPlayer.close();
if (currentPlayingFile != null) {
onSongEnd(currentPlayingFile);
public void setPosition(Player airsonicPlayer, int positionInSeconds) {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
throw new UnsupportedOperationException();
case JAVA_JUKEBOX:
jukeboxJavaService.setPosition(airsonicPlayer,positionInSeconds);
break;
}
}
if (file != null) {
int duration = file.getDurationSeconds() == null ? 0 : file.getDurationSeconds() - offset;
TranscodingService.Parameters parameters = new TranscodingService.Parameters(file, new VideoTranscodingSettings(0, 0, offset, duration, false));
String command = settingsService.getJukeboxCommand();
parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false));
in = transcodingService.getTranscodedInputStream(parameters);
audioPlayer = new AudioPlayer(in, this);
audioPlayer.setGain(gain);
audioPlayer.play();
onSongStart(file);
public float getGain(Player airsonicPlayer) {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
return jukeboxLegacySubsonicService.getGain();
case JAVA_JUKEBOX:
return jukeboxJavaService.getGain(airsonicPlayer);
}
return 0;
}
currentPlayingFile = file;
} catch (Exception x) {
LOG.error("Error in jukebox: " + x, x);
IOUtils.closeQuietly(in);
/**
* This method should be removed when the jukebox is controlled only through rest api.
*
* @param airsonicPlayer
* @param offset
* @throws Exception
*/
@Deprecated
public void updateJukebox(Player airsonicPlayer, int offset) throws Exception {
if (airsonicPlayer.getTechnology().equals(PlayerTechnology.JUKEBOX)) {
jukeboxLegacySubsonicService.updateJukebox(airsonicPlayer,offset);
}
}
public synchronized void stateChanged(AudioPlayer audioPlayer, AudioPlayer.State state) {
if (state == AudioPlayer.State.EOM) {
player.getPlayQueue().next();
MediaFile result;
synchronized (player.getPlayQueue()) {
result = player.getPlayQueue().getCurrentFile();
}
play(result, 0);
public int getPosition(Player airsonicPlayer) {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
return jukeboxLegacySubsonicService.getPosition();
case JAVA_JUKEBOX:
return jukeboxJavaService.getPosition(airsonicPlayer);
}
return 0;
}
public synchronized float getGain() {
return gain;
/**
* This method is only here due to legacy considerations and should be removed
* if the jukeboxLegacySubsonicService is removed.
* @param airsonicPlayer
* @return
*/
@Deprecated
public boolean canControl(Player airsonicPlayer) {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
if (jukeboxLegacySubsonicService.getPlayer() == null) {
return false;
} else {
return jukeboxLegacySubsonicService.getPlayer().getId().equals(airsonicPlayer.getId());
}
public synchronized int getPosition() {
return audioPlayer == null ? 0 : offset + audioPlayer.getPosition();
case JAVA_JUKEBOX:
return true;
}
return false;
}
/**
* Returns the player which currently uses the jukebox.
* Plays the playQueue of a jukebox player starting at the first item of the queue.
*
* @return The player, may be {@code null}.
* @param airsonicPlayer
* @throws Exception
*/
public Player getPlayer() {
return player;
public void play(Player airsonicPlayer) throws Exception {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
jukeboxLegacySubsonicService.updateJukebox(airsonicPlayer,0);
break;
case JAVA_JUKEBOX:
jukeboxJavaService.play(airsonicPlayer);
break;
}
private void onSongStart(MediaFile file) {
LOG.info(player.getUsername() + " starting jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\"");
status = statusService.createStreamStatus(player);
status.setFile(file.getFile());
status.addBytesTransfered(file.getFileSize());
mediaFileService.incrementPlayCount(file);
scrobble(file, false);
}
private void onSongEnd(MediaFile file) {
LOG.info(player.getUsername() + " stopping jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\"");
if (status != null) {
statusService.removeStreamStatus(status);
}
scrobble(file, true);
}
private void scrobble(MediaFile file, boolean submission) {
if (player.getClientId() == null) { // Don't scrobble REST players.
audioScrobblerService.register(file, player.getUsername(), submission, null);
public void start(Player airsonicPlayer) throws Exception {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
jukeboxLegacySubsonicService.updateJukebox(airsonicPlayer,0);
break;
case JAVA_JUKEBOX:
jukeboxJavaService.start(airsonicPlayer);
break;
}
}
public synchronized void setGain(float gain) {
this.gain = gain;
if (audioPlayer != null) {
audioPlayer.setGain(gain);
public void stop(Player airsonicPlayer) throws Exception {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
jukeboxLegacySubsonicService.updateJukebox(airsonicPlayer,0);
break;
case JAVA_JUKEBOX:
jukeboxJavaService.stop(airsonicPlayer);
break;
}
}
public void setTranscodingService(TranscodingService transcodingService) {
this.transcodingService = transcodingService;
public void skip(Player airsonicPlayer,int index,int offset) throws Exception {
switch (airsonicPlayer.getTechnology()) {
case JUKEBOX:
jukeboxLegacySubsonicService.updateJukebox(airsonicPlayer,offset);
break;
case JAVA_JUKEBOX:
jukeboxJavaService.skip(airsonicPlayer,index,offset);
break;
}
public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
this.audioScrobblerService = audioScrobblerService;
}
public void setStatusService(StatusService statusService) {
this.statusService = statusService;
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
/* properties setters */
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
public void setJukeboxLegacySubsonicService(JukeboxLegacySubsonicService jukeboxLegacySubsonicService) {
this.jukeboxLegacySubsonicService = jukeboxLegacySubsonicService;
}
public void setMediaFileService(MediaFileService mediaFileService) {
this.mediaFileService = mediaFileService;
public void setJukeboxJavaService(JukeboxJavaService jukeboxJavaService) {
this.jukeboxJavaService = jukeboxJavaService;
}
}

@ -182,8 +182,12 @@ public class PlayerService {
* @return The player with the given ID, or <code>null</code> if no such player exists.
*/
public Player getPlayerById(String id) {
if (StringUtils.isBlank(id)) {
return null;
} else {
return playerDao.getPlayerById(id);
}
}
/**
* Returns whether the given player is connected.

@ -0,0 +1,17 @@
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="add-player-mixer_001" author="biconou">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="player" columnName="mixer" />
</not>
</preConditions>
<addColumn tableName="player">
<column name="mixer" type="${varchar_type}">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

@ -2,5 +2,6 @@
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="add-player-mixer.xml" relativeToChangelogFile="true"/>
<include file="mediaelement.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

@ -452,11 +452,14 @@ playersettings.technology.web.title=Web player
playersettings.technology.external.title=External player
playersettings.technology.external_with_playlist.title=External player with playlist
playersettings.technology.jukebox.title=Jukebox
playersettings.technology.java_jukebox.title=Java Jukebox
playersettings.technology.web.text=Play music directly in the web browser using the integrated Flash player.
playersettings.technology.external.text=Play music in your favorite player, such as WinAmp or Windows Media Player.
playersettings.technology.external_with_playlist.text=Same as above, but the playlist is managed by the player, rather than the Airsonic server. In this mode, skipping within songs is possible.
playersettings.technology.jukebox.text=Play music directly on the audio device of the Airsonic server. (Authorized users only).
playersettings.technology.java_jukebox.text=Jukebox player that uses the Java Sound API. (Authorized users only).
playersettings.name=Player name
playersettings.javaJukeboxMixer=Audio device
playersettings.maxbitrate=Max bitrate
playersettings.notranscoder=<em>Notice:</em> Transcoders does not appear to be installed.<br>Click Help button for more information.
playersettings.autocontrol=Control player automatically

@ -0,0 +1,35 @@
<div id="javaJukeboxPlayerControlBar" class="bgcolor2" style="position:fixed; bottom:0; width:100%;padding-top:10px;padding-bottom: 5px">
<table style="white-space:nowrap;">
<tr style="white-space:nowrap;">
<c:if test="${model.user.settingsRole and fn:length(model.players) gt 1}">
<td style="padding-right: 5px">
<select name="player" onchange="location='playQueue.view?player=' + options[selectedIndex].value;">
<c:forEach items="${model.players}" var="player">
<option ${player.id eq model.player.id ? "selected" : ""} value="${player.id}">${player.shortDescription}</option>
</c:forEach>
</select>
</td>
</c:if>
<td>
<img id="startIcon" src="<spring:theme code="castPlayImage"/>" onclick="onJavaJukeboxStart()" style="cursor:pointer">
<img id="pauseIcon" src="<spring:theme code="castPauseImage"/>" onclick="onJavaJukeboxStop()" style="cursor:pointer; display:none">
</td>
<td><span id="playingPositionDisplay" class="javaJukeBoxPlayerControlBarSongTime"/></td>
<td style="white-space:nowrap;">
<div id="javaJukeboxSongPositionSlider"></div>
</td>
<td><span id="playingDurationDisplay" class="javaJukeBoxPlayerControlBarSongTime"/></td>
<td style="white-space:nowrap;">
<img src="<spring:theme code="volumeImage"/>" alt="">
</td>
<td style="white-space:nowrap;">
<div id="javaJukeboxVolumeSlider" style="width:80px;height:4px"></div>
</td>
</tr>
</table>
</div>
<script type="text/javascript" src="<c:url value='/script/playQueue/javaJukeboxPlayerControlBar.js'/>"></script>
<script type="text/javascript">
initJavaJukeboxPlayerControlBar();
</script>

@ -3,6 +3,7 @@
<html><head>
<%@ include file="head.jsp" %>
<%@ include file="jquery.jsp" %>
<script type="text/javascript" src="<c:url value="/script/moment-2.18.1.min.js"/>"></script>
<link type="text/css" rel="stylesheet" href="<c:url value="/script/webfx/luna.css"/>">
<script type="text/javascript" src="<c:url value="/script/scripts-2.0.js"/>"></script>
<script type="text/javascript" src="<c:url value="/dwr/interface/nowPlayingService.js"/>"></script>
@ -257,6 +258,9 @@
</c:when>
<c:otherwise>
currentStreamUrl = songs[index].streamUrl;
if (isJavaJukeboxPresent()) {
updateJavaJukeboxPlayerControlBar(songs[index]);
}
playQueueService.skip(index, playQueueCallback);
</c:otherwise>
</c:choose>
@ -399,6 +403,10 @@
});
}
function isJavaJukeboxPresent() {
return $("#javaJukeboxPlayerControlBarContainer").length==1;
}
function playQueueCallback(playQueue) {
songs = playQueue.entries;
repeatEnabled = playQueue.repeatEnabled;
@ -446,6 +454,9 @@
}
if ($("#currentImage" + id) && song.streamUrl == currentStreamUrl) {
$("#currentImage" + id).show();
if (isJavaJukeboxPresent()) {
updateJavaJukeboxPlayerControlBar(song);
}
}
if ($("#title" + id)) {
$("#title" + id).html(song.title);
@ -658,7 +669,14 @@
</script>
<div class="bgcolor2" style="position:fixed; bottom:0; width:100%;padding-top:10px;">
<c:choose>
<c:when test="${model.player.javaJukebox}">
<div id="javaJukeboxPlayerControlBarContainer">
<%@ include file="javaJukeboxPlayerControlBar.jspf" %>
</div>
</c:when>
<c:otherwise>
<div class="bgcolor2" style="position:fixed; bottom:0; width:100%;padding-top:10px;">
<table style="white-space:nowrap;">
<tr style="white-space:nowrap;">
<c:if test="${model.user.settingsRole and fn:length(model.players) gt 1}">
@ -766,7 +784,10 @@
</td>
</tr></table>
</div>
</div>
</c:otherwise>
</c:choose>
<h2 style="float:left"><fmt:message key="playlist.more.playlist"/></h2>
<h2 id="songCountAndDuration" style="float:right;padding-right:1em"></h2>

@ -10,6 +10,34 @@
<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
<script lang="javascript">
function hideAllTechnologyDepends() {
$('.technologyDepends').hide();
}
function showTechnologyDepends(technologyName) {
var selector = '.technologyDepends.' + technologyName;
$(selector).show();
}
$(document).ready(function() {
$('.technologyRadio').click(function() {
hideAllTechnologyDepends();
var technologyName = $(this).val();
showTechnologyDepends(technologyName);
});
hideAllTechnologyDepends();
$('.technologyRadio:checked').each(function() {
var technologyName = $(this).val();
showTechnologyDepends(technologyName);
});
});
</script>
<c:import url="settingsHeader.jsp">
<c:param name="cat" value="player"/>
<c:param name="toast" value="${settings_toast}"/>
@ -60,7 +88,7 @@
<tr>
<td class="ruleTableHeader">
<form:radiobutton id="radio-${technologyName}" path="technologyName" value="${technologyHolder.name}"/>
<form:radiobutton class="technologyRadio" id="radio-${technologyName}" path="technologyName" value="${technologyHolder.name}"/>
<b><label for="radio-${technologyName}">${technologyName}</label></b>
</td>
<td class="ruleTableCell" style="width:40em">
@ -87,14 +115,27 @@
</table>
<table class="indent" style="border-spacing:3pt;">
<tr>
<td><fmt:message key="playersettings.name"/></td>
<td><form:input path="name" size="16"/></td>
<td colspan="2"><c:import url="helpToolTip.jsp"><c:param name="topic" value="playername"/></c:import></td>
</tr>
<tr>
<tr class="technologyDepends JAVA_JUKEBOX">
<td><fmt:message key="playersettings.javaJukeboxMixer"/></td>
<td>
<form:select path="javaJukeboxMixer">
<c:forEach items="${command.javaJukeboxMixers}" var="javaJukeboxMixer">
<form:option value="${javaJukeboxMixer}" label="${javaJukeboxMixer}"/>
</c:forEach>
</form:select>
</td>
<td colspan="2"></td>
</tr>
<%// Max bitrate %>
<tr class="technologyDepends WEB EXTERNAL EXTERNAL_WITH_PLAYLIST JUKEBOX">
<td><fmt:message key="playersettings.maxbitrate"/></td>
<td>
<form:select path="transcodeSchemeName" cssStyle="width:8em">
@ -112,11 +153,9 @@
</c:if>
</td>
</tr>
</table>
<table class="indent" style="border-spacing:3pt">
<table class="indent technologyDepends WEB EXTERNAL EXTERNAL_WITH_PLAYLIST JUKEBOX" style="border-spacing:3pt">
<tr>
<td>
<form:checkbox path="dynamicIp" id="dynamicIp" cssClass="checkbox"/>
@ -143,7 +182,7 @@
</table>
<c:if test="${not empty command.allTranscodings}">
<table class="indent">
<table class="indent technologyDepends WEB EXTERNAL EXTERNAL_WITH_PLAYLIST JUKEBOX">
<tr><td><b><fmt:message key="playersettings.transcodings"/></b></td></tr>
<c:forEach items="${command.allTranscodings}" var="transcoding" varStatus="loopStatus">
<c:if test="${loopStatus.count % 3 == 1}"><tr></c:if>
@ -158,9 +197,8 @@
<input type="submit" value="<fmt:message key="common.save"/>" style="margin-top:1em;margin-right:0.3em">
<input type="button" value="<fmt:message key="common.cancel"/>" style="margin-top:1em" onclick="location.href='nowPlaying.view'">
</form:form>
</c:otherwise>
</form:form>
</c:otherwise>
</c:choose>
<c:if test="${settings_reload}">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,102 @@
var songPlayingTimerId = null;
var javaJukeboxPlayerModel = {
currentStreamUrl : null,
playing : false,
songDuration : null,
songPosition : 0
}
function refreshView() {
if (javaJukeboxPlayerModel.playing == true) {
if (songPlayingTimerId == null) {
songPlayingTimerId = setInterval(songPlayingTimer, 1000);
}
document.getElementById('startIcon').style.display = 'none';
document.getElementById('pauseIcon').style.display = 'block';
} else {
if (songPlayingTimerId != null) {
clearInterval(songPlayingTimerId);
songPlayingTimerId = null;
}
document.getElementById('pauseIcon').style.display = 'none';
document.getElementById('startIcon').style.display = 'block';
}
if (javaJukeboxPlayerModel.songDuration == null) {
$("#playingDurationDisplay").html("-:--");
} else {
$("#playingDurationDisplay").html(songTimeAsString(javaJukeboxPlayerModel.songDuration));
}
$("#playingPositionDisplay").html(songTimeAsString(javaJukeboxPlayerModel.songPosition));
$("#javaJukeboxSongPositionSlider").slider("value",javaJukeboxPlayerModel.songPosition);
}
function onJavaJukeboxStart() {
playQueueService.start();
javaJukeboxPlayerModel.playing = true;
refreshView();
}
function onJavaJukeboxStop() {
playQueueService.stop();
javaJukeboxPlayerModel.playing = false;
refreshView();
}
function onJavaJukeboxVolumeChanged() {
var value = $("#javaJukeboxVolumeSlider").slider("value");
var gain = value / 100;
playQueueService.setGain(gain);
}
function onJavaJukeboxPositionChanged() {
var pos = $("#javaJukeboxSongPositionSlider").slider("value");
playQueueService.setJukeboxPosition(pos);
javaJukeboxPlayerModel.songPosition = pos;
refreshView();
}
function updateJavaJukeboxPlayerControlBar(song){
if (song != null) {
var playingStream = song.streamUrl;
if (playingStream != javaJukeboxPlayerModel.currentStreamUrl) {
javaJukeboxPlayerModel.currentStreamUrl = playingStream;
newSongPlaying(song);
}
}
}
function songTimeAsString(timeInSeconds) {
var m = moment.duration(timeInSeconds, 'seconds');
var seconds = m.seconds();
var secondsAsString = seconds;
if (seconds < 10) {
secondsAsString = "0" + seconds;
}
return m.minutes() + ":" + secondsAsString;
}
function newSongPlaying(song) {
javaJukeboxPlayerModel.songDuration = song.duration;
$("#javaJukeboxSongPositionSlider").slider({max: javaJukeboxPlayerModel.songDuration, value: 0, animate: "fast", range: "min"});
javaJukeboxPlayerModel.playing = true;
javaJukeboxPlayerModel.songPosition = 0;
refreshView();
}
function songPlayingTimer() {
javaJukeboxPlayerModel.songPosition += 1;
refreshView();
}
function initJavaJukeboxPlayerControlBar() {
$("#javaJukeboxSongPositionSlider").slider({max: 100, value: 0, animate: "fast", range: "min"});
$("#javaJukeboxSongPositionSlider").slider("value",0);
$("#javaJukeboxSongPositionSlider").on("slidestop", onJavaJukeboxPositionChanged);
$("#javaJukeboxVolumeSlider").slider({max: 100, value: 50, animate: "fast", range: "min"});
$("#javaJukeboxVolumeSlider").on("slidestop", onJavaJukeboxVolumeChanged);
refreshView();
}

@ -427,3 +427,19 @@ img {
color: black;
}
/*
* Styles related to java jukebox controls
*/
.javaJukeBoxPlayerControlBarSongTime {
width : 50px;
font-size: 80%;
text-align: right;
padding-left: 10px;
padding-right: 10px;
}
#javaJukeboxSongPositionSlider {
width : 200px;
height : 4px;
}

Loading…
Cancel
Save