mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 10:43:20 +00:00
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:
74
code/modules/media/media_machinery.dm
Normal file
74
code/modules/media/media_machinery.dm
Normal 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()
|
||||
..()
|
||||
|
||||
27
code/modules/media/media_player_html5.dm
Normal file
27
code/modules/media/media_player_html5.dm
Normal 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>
|
||||
"}
|
||||
27
code/modules/media/media_player_vlc.dm
Normal file
27
code/modules/media/media_player_vlc.dm
Normal 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>
|
||||
"}
|
||||
14
code/modules/media/media_player_wmp.dm
Normal file
14
code/modules/media/media_player_wmp.dm
Normal 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>"}
|
||||
57
code/modules/media/media_tracks.dm
Normal file
57
code/modules/media/media_tracks.dm
Normal 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
|
||||
189
code/modules/media/mediamanager.dm
Normal file
189
code/modules/media/mediamanager.dm
Normal 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)
|
||||
Reference in New Issue
Block a user