Implements browser streaming media jukeboxes

Ports media code from vgstation, updates it for this codebase and modernizes it.
* Changes jukeboxes to load songs using an embedded browser instead of sending over BYOND's sound channels.  This means they load out of band without lagging the server.  Also songs can be resumed mid-song, so leaving and returning to an area doesn't start the music over.
* The old WMP and VLC player modes from /vg are still supported, but adds a new default mode using HTML5 audio to play the music.
  * WMP - The oldest, still works on IE on windows, but only there, and Microsoft could break it any second.
  * VLC - Works on all platforms, but requires user to have VLC pre-installed on their computer.  Uses a scary plugin.
  * HTML5 - New default, It is cross platform but doesn't require you to have VLC installed to work.  Also caches songs locally even between rounds.
* Changed jukebox.txt to be jukebox.json, now can include artist information as well.  Must include the duration of songs as well.
* For HTML5 audio compatibility, use only MP3 files, its the only format supported on all browsers.
* Jukebox itself is also upgraded, instead of just repeating the same song over and over it can actually advance to the next song when one is done playing.  Has a few modes including random, next, and single song.
* Jukeboxes have a UI improvement, and have a volume control.
* Three new settings are added to global settings in character setup
  * Jukebox music on/off toggles jukebox music independently of normal station ambience.  Now you can hear ambience but not music. (or vice versa if you wanted...)
  * Jukebox music volume.  Control the relative volume of jukebox music.   Actual volume is player's configured volume * jukebox's configured volume.
  * Media player type.  Choose between WMP, VLC, and HTML5
* Fixes a few bugs in the /vg code.
This commit is contained in:
Leshana
2017-03-23 20:22:50 -04:00
parent 60285dad52
commit c63c68c9a8
13 changed files with 631 additions and 141 deletions

View File

@@ -0,0 +1,74 @@
// Machinery serving as a media source.
/obj/machinery/media
var/playing = 0 // Am I playing right now?
var/media_url = "" // URL of media I am playing
var/media_start_time = 0 // world.time when it started playing
var/volume = 1 // 0 - 1 for ease of coding.
var/area/master_area // My area
// ~Leshana - Transmitters unimplemented
// Notify everyone in the area of new music.
// YOU MUST SET MEDIA_URL AND MEDIA_START_TIME YOURSELF!
/obj/machinery/media/proc/update_music()
update_media_source()
// Bail if we lost connection to master.
if(!master_area)
return
// Send update to clients.
for(var/mob/M in mobs_in_area(master_area))
if(M && M.client)
M.update_music()
/obj/machinery/media/proc/update_media_source()
var/area/A = get_area_master(src)
if(!A)
return
// Check if there's a media source already.
if(A.media_source && A.media_source != src) // If it does, the new media source replaces it. basically, the last media source arrived gets played on top.
A.media_source.disconnect_media_source() // You can turn a media source off and on for it to come back on top.
A.media_source = src
master_area = A
return
else
A.media_source = src
master_area = A
/obj/machinery/media/proc/disconnect_media_source()
var/area/A = get_area_master(src)
// Sanity
if(!A)
master_area = null
return
// Check if there's a media source already.
if(A && A.media_source && A.media_source != src)
master_area = null
return
// Update Media Source.
A.media_source = null
// Clients
for(var/mob/M in mobs_in_area(A))
if(M && M.client)
M.update_music()
master_area = null
/obj/machinery/media/Move()
..()
if(anchored)
update_music()
/obj/machinery/media/forceMove(var/atom/destination)
disconnect_media_source()
..()
if(anchored)
update_music()
/obj/machinery/media/initialize()
..()
update_media_source()
/obj/machinery/media/Destroy()
disconnect_media_source()
..()

View File

@@ -0,0 +1,27 @@
// IT IS FINALLY TIME. IT IS HERE. Converted to HTML5 <audio> - Leshana
var/const/PLAYER_HTML5_HTML={"<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=11">
<script type="text/javascript">
function noErrorMessages () { return true; }
window.onerror = noErrorMessages;
function SetMusic(url, time, volume) {
var player = document.getElementById('player');
// IE can't handle us setting the time before it loads, so we must wait for asychronous load
var setTime = function () {
player.removeEventListener("canplay", setTime); // One time only!
player.volume = volume;
player.currentTime = time;
player.play();
}
if(url != "") player.addEventListener("canplay", setTime, false);
player.src = url;
}
</script>
</head>
<body>
<audio id="player"></audio>
</body>
</html>
"}

View File

@@ -0,0 +1,27 @@
// Open up VLC and play musique.
// Converted to VLC for cross-platform and ogg support. - N3X
var/const/PLAYER_VLC_HTML={"
<object classid="clsid:9BE31822-FDAD-461B-AD51-BE1D1C159921" codebase="http://download.videolan.org/pub/videolan/vlc/last/win32/axvlc.cab" id="player"></object>
<script>
function noErrorMessages () { return true; }
window.onerror = noErrorMessages;
function SetMusic(url, time, volume) {
var vlc = document.getElementById('player');
// Stop playing
vlc.playlist.stop();
// Clear playlist
vlc.playlist.items.clear();
// Add new playlist item.
var id = vlc.playlist.add(url);
// Play playlist item
vlc.playlist.playItem(id);
vlc.input.time = time*1000; // VLC takes milliseconds.
vlc.audio.volume = volume*100; // \[0-200]
}
</script>
"}

View File

@@ -0,0 +1,14 @@
// Legacy player using Windows Media Player OLE object.
// I guess it will work in IE on windows, and BYOND uses IE on windows, so alright!
var/const/PLAYER_WMP_HTML={"
<OBJECT id='player' CLASSID='CLSID:6BF52A52-394A-11d3-B153-00C04F79FAA6' type='application/x-oleobject'></OBJECT>
<script>
function noErrorMessages () { return true; }
window.onerror = noErrorMessages;
function SetMusic(url, time, volume) {
var player = document.getElementById('player');
player.URL = url;
player.Controls.currentPosition = +time;
player.Settings.volume = +volume;
}
</script>"}

View File

@@ -0,0 +1,57 @@
//
// Load the list of available music tracks for the jukebox (or other things that use music)
//
// Music track available for playing in a media machine.
/datum/track
var/url // URL to load song from
var/title // Song title
var/artist // Song's creator
var/duration // Song length in deciseconds
var/secret // Show up in regular playlist or secret playlist?
/datum/track/New(var/url, var/title, var/duration, var/artist = "", var/secret = 0)
src.url = url
src.title = title
src.artist = artist
src.duration = duration
src.secret = secret
/datum/track/proc/display()
var str = "\"[title]\""
if(artist)
str += " by [artist]"
return str
/datum/track/proc/toNanoList()
return list("ref" = "\ref[src]", "title" = title, "artist" = artist, "duration" = duration)
// Global list holding all configured jukebox tracks
var/global/list/all_jukebox_tracks = list()
// Read the jukebox configuration file on system startup.
/hook/startup/proc/load_jukebox_tracks()
var/jukebox_track_file = "config/jukebox.json"
if(!fexists(jukebox_track_file))
warning("File not found: [jukebox_track_file]")
return
var/list/jsonData = json_decode(file2text(jukebox_track_file))
if(!istype(jsonData))
warning("Failed to read tracks from [jukebox_track_file], json_decode failed.")
for(var/entry in jsonData)
if(!istext(entry["url"]))
warning("[jukebox_track_file] entry [entry]: bad or missing 'url'")
continue
if(!istext(entry["title"]))
warning("[jukebox_track_file] entry [entry]: bad or missingg 'title'")
continue
if(!isnum(entry["duration"]))
warning("[jukebox_track_file] entry [entry]: bad or missing 'duration'")
continue
var/datum/track/T = new(entry["url"], entry["title"], entry["duration"])
if(istext(entry["artist"]))
T.artist = entry["artist"]
T.secret = entry["secret"] ? 1 : 0
all_jukebox_tracks += T
return 1

View File

@@ -0,0 +1,189 @@
/**********************
* AWW SHIT IT'S TIME FOR RADIO
*
* Concept stolen from D2K5
* Rewritten by N3X15 for vgstation
* Adapted by Leshana for VOREStation
***********************/
// Uncomment to test the mediaplayer
// #define DEBUG_MEDIAPLAYER
#ifdef DEBUG_MEDIAPLAYER
#define MP_DEBUG(x) owner << x
#warning Please comment out #define DEBUG_MEDIAPLAYER before committing.
#else
#define MP_DEBUG(x)
#endif
/proc/stop_all_media()
for(var/mob/M in mob_list)
if(M && M.client)
M.stop_all_music()
// Set up player on login to a mob.
// This means they get a new media manager every time they switch mobs!
// Is this wasteful? Granted switching mobs doesn't happen very often so maybe its fine.
// TODO - While this direct override might technically be faster, probably better code to use observer or hooks ~Leshana
/mob/Login()
. = ..()
ASSERT(src.client)
src.client.media = new /datum/media_manager(src.client)
src.client.media.open()
src.client.media.update_music()
// Stop media when the round ends. I guess so it doesn't play forever or something (for some reason?)
/hook/roundend/proc/stop_all_media()
log_debug("Stopping all playing media...")
// Stop all music.
stop_all_media()
// SHITTY HACK TO AVOID RACE CONDITION WITH SERVER REBOOT.
sleep(10) // TODO - Leshana - see if this is needed
// Update when moving between areas.
// TODO - While this direct override might technically be faster, probably better code to use observer or hooks ~Leshana
/area/Entered(var/mob/living/M)
// Note, we cannot call ..() first, because it would update lastarea.
if(!istype(M))
return ..()
// Optimization, no need to call update_music() if both are null (or same instance, strange as that would be)
if(M.lastarea && M.lastarea.media_source == src.media_source)
return ..()
if(M.client && M.client.media && !M.client.media.forced)
M.update_music()
return ..()
//
// ### Media variable on /client ###
/client
// Set on Login
var/datum/media_manager/media = null
/client/verb/change_volume()
set name = "Set Volume"
set category = "OOC"
set desc = "Set jukebox volume"
set_new_volume(usr)
/client/proc/set_new_volume(var/mob/user)
if(deleted(src.media) || !istype(src.media))
to_chat(user, "<span class='warning'>You have no media datum to change, if you're not in the lobby tell an admin.</span>")
return
var/value = input("Choose your Jukebox volume.", "Jukebox volume", media.volume)
value = round(max(0, min(100, value)))
media.update_volume(value)
//
// ### Media procs on mobs ###
// These are all convenience functions, simple delegations to the media datum on mob.
// But their presense and null checks make other coder's life much easier.
//
/mob/proc/update_music()
if (client && client.media && !client.media.forced)
client.media.update_music()
/mob/proc/stop_all_music()
if (client && client.media)
client.media.stop_music()
/mob/proc/force_music(var/url, var/start, var/volume=1)
if (client && client.media)
if(url == "")
client.media.forced = 0
client.media.update_music()
else
client.media.forced = 1
client.media.push_music(url, start, volume)
return
//
// ### Define media source to areas ###
// Each area may have at most one media source that plays songs into that area.
// We keep track of that source so any mob entering the area can lookup what to play.
//
/area
// For now, only one media source per area allowed
// Possible Future: turn into a list, then only play the first one that's playing.
var/obj/machinery/media/media_source = null
//
// ### Media Manager Datum
//
/datum/media_manager
var/url = "" // URL of currently playing media
var/start_time = 0 // world.time when it started playing *in the source* (Not when started playing for us)
var/source_volume = 1 // Volume as set by source. Actual volume = "volume * source_volume"
var/rate = 1 // Playback speed. For Fun(tm)
var/volume = 50 // Client's volume modifier. Actual volume = "volume * source_volume"
var/client/owner // Client this is actually running in
var/forced=0 // If true, current url overrides area media sources
var/playerstyle // Choice of which player plugin to use
var/const/WINDOW_ID = "rpane.mediapanel" // Which elem in skin.dmf to use
/datum/media_manager/New(var/client/C)
ASSERT(istype(C))
src.owner = C
// Actually pop open the player in the background.
/datum/media_manager/proc/open()
if(!owner.prefs)
return
if(isnum(owner.prefs.media_volume))
volume = owner.prefs.media_volume
switch(owner.prefs.media_player)
if(0)
playerstyle = PLAYER_VLC_HTML
if(1)
playerstyle = PLAYER_WMP_HTML
if(2)
playerstyle = PLAYER_HTML5_HTML
owner << browse(null, "window=[WINDOW_ID]")
owner << browse(playerstyle, "window=[WINDOW_ID]")
send_update()
// Tell the player to play something via JS.
/datum/media_manager/proc/send_update()
if(!(owner.prefs))
return
if(!owner.is_preference_enabled(/datum/client_preference/play_jukebox) && url != "")
return // Don't send anything other than a cancel to people with SOUND_STREAMING pref disabled
MP_DEBUG("<span class='good'>Sending update to mediapanel ([url], [(world.time - start_time) / 10], [volume * source_volume])...</span>")
owner << output(list2params(list(url, (world.time - start_time) / 10, volume * source_volume)), "[WINDOW_ID]:SetMusic")
/datum/media_manager/proc/push_music(var/targetURL, var/targetStartTime, var/targetVolume)
if (url != targetURL || abs(targetStartTime - start_time) > 1 || abs(targetVolume - source_volume) > 0.1 /* 10% */)
url = targetURL
start_time = targetStartTime
source_volume = Clamp(targetVolume, 0, 1)
send_update()
/datum/media_manager/proc/stop_music()
push_music("", 0, 1)
/datum/media_manager/proc/update_volume(var/value)
volume = value
send_update()
// Scan for media sources and use them.
/datum/media_manager/proc/update_music()
var/targetURL = ""
var/targetStartTime = 0
var/targetVolume = 0
if (forced || !owner || !owner.mob)
return
var/area/A = get_area_master(owner.mob)
if(!A)
MP_DEBUG("client=[owner], mob=[owner.mob] not in an area! loc=[owner.mob.loc]. Aborting.")
stop_music()
return
var/obj/machinery/media/M = A.media_source
if(M && M.playing)
targetURL = M.media_url
targetStartTime = M.media_start_time
targetVolume = M.volume
//MP_DEBUG("Found audio source: [M.media_url] @ [(world.time - start_time) / 10]s.")
push_music(targetURL, targetStartTime, targetVolume)