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

@@ -1,10 +1,12 @@
datum/track
var/title
var/sound
//
// Media Player Jukebox
// Rewritten by Leshana from existing Polaris code, merging in D2K5 and N3X15 work
//
datum/track/New(var/title_name, var/audio)
title = title_name
sound = audio
#define JUKEMODE_NEXT 1 // Advance to next song in the track list
#define JUKEMODE_RANDOM 2 // Not shuffle, randomly picks next each time.
#define JUKEMODE_REPEAT_SONG 3 // Play the same song over and over
#define JUKEMODE_PLAY_ONCE 4 // Play, then stop.
/obj/machinery/media/jukebox/
name = "space jukebox"
@@ -19,33 +21,17 @@ datum/track/New(var/title_name, var/audio)
active_power_usage = 100
circuit = /obj/item/weapon/circuitboard/jukebox
var/playing = 0
// Vars for hacking
var/datum/wires/jukebox/wires = null
var/hacked = 0 // Whether to show the hidden songs or not
var/freq = 0
var/freq = 0 // Currently no effect, will return in phase II of mediamanager.
var/datum/track/current_track
var/list/datum/track/tracks = list(
new/datum/track("Beyond", 'sound/ambience/ambispace.ogg'),
new/datum/track("Clouds of Fire", 'sound/music/clouds.s3m'),
new/datum/track("D`Bert", 'sound/music/title2.ogg'),
new/datum/track("D`Fort", 'sound/ambience/song_game.ogg'),
new/datum/track("Floating", 'sound/music/main.ogg'),
new/datum/track("Endless Space", 'sound/music/space.ogg'),
new/datum/track("Part A", 'sound/misc/TestLoop1.ogg'),
new/datum/track("Scratch", 'sound/music/title1.ogg'),
new/datum/track("Trai`Tor", 'sound/music/traitor.ogg'),
)
// Only visible if hacked
var/list/datum/track/secret_tracks = list(
new/datum/track("Clown", 'sound/music/clown.ogg'),
new/datum/track("Space Asshole", 'sound/music/space_asshole.ogg'),
new/datum/track("Thunderdome", 'sound/music/THUNDERDOME.ogg'),
new/datum/track("Russkiy rep Diskoteka", 'sound/music/russianrapdisco.ogg')
)
var/loop_mode = JUKEMODE_PLAY_ONCE // Behavior when finished playing a song
var/max_queue_len = 3 // How many songs are we allowed to queue up?
var/datum/track/current_track // Currently playing song
var/list/datum/track/queue = list() // Queued songs
var/list/datum/track/tracks = list() // Available tracks
var/list/datum/track/secret_tracks = list() // Only visible if hacked
/obj/machinery/media/jukebox/New()
..()
@@ -54,11 +40,70 @@ datum/track/New(var/title_name, var/audio)
update_icon()
/obj/machinery/media/jukebox/Destroy()
StopPlaying()
qdel(wires)
wires = null
..()
// On initialization, copy our tracks from the global list
/obj/machinery/media/jukebox/initialize()
..()
if(all_jukebox_tracks.len < 1)
stat |= BROKEN // No tracks configured this round!
return
// Ootherwise load from the global list!
for(var/datum/track/T in all_jukebox_tracks)
if(T.secret)
secret_tracks |= T
else
tracks |= T
return
/obj/machinery/media/jukebox/process()
if(!playing)
return
if(inoperable())
disconnect_media_source()
playing = 0
return
// If the current track isn't finished playing, let it keep going
if(current_track && world.time < media_start_time + current_track.duration)
return
// Otherwise time to pick a new one!
if(queue.len > 0)
current_track = queue[1]
queue.Cut(1, 2) // Remove the item we just took off the list
else
// Oh... nothing in queue? Well then pick next according to our rules
switch(loop_mode)
if(JUKEMODE_NEXT)
var/curTrackIndex = max(1, tracks.Find(current_track))
var/newTrackIndex = (curTrackIndex % tracks.len) + 1 // Loop back around if past end
current_track = tracks[newTrackIndex]
if(JUKEMODE_RANDOM)
var/previous_track = current_track
do
current_track = pick(tracks)
while(current_track == previous_track && tracks.len > 1)
if(JUKEMODE_REPEAT_SONG)
current_track = current_track
if(JUKEMODE_PLAY_ONCE)
current_track = null
playing = 0
update_icon()
updateDialog()
start_stop_song()
// Tells the media manager to start or stop playing based on current settings.
/obj/machinery/media/jukebox/proc/start_stop_song()
if(current_track && playing)
media_url = current_track.url
media_start_time = world.time
visible_message("<span class='notice'>\The [src] begins to play [current_track.display()].</span>")
else
media_url = ""
media_start_time = 0
update_music()
/obj/machinery/media/jukebox/proc/set_hacked(var/newhacked)
if (hacked == newhacked) return
hacked = newhacked
@@ -80,13 +125,16 @@ datum/track/New(var/title_name, var/audio)
if(istype(W, /obj/item/device/multitool))
return wires.Interact(user)
if(istype(W, /obj/item/weapon/wrench))
if(playing)
StopPlaying()
user.visible_message("<span class='warning'>[user] has [anchored ? "un" : ""]secured \the [src].</span>", "<span class='notice'>You [anchored ? "un" : ""]secure \the [src].</span>")
anchored = !anchored
playsound(src.loc, 'sound/items/Ratchet.ogg', 50, 1)
power_change()
update_icon()
if(!anchored)
playing = 0
disconnect_media_source()
else
update_media_source()
return
return ..()
@@ -125,16 +173,23 @@ datum/track/New(var/title_name, var/audio)
usr << "<span class='warning'>You must secure \the [src] first.</span>"
return
if(stat & (NOPOWER|BROKEN))
if(inoperable())
usr << "\The [src] doesn't appear to function."
return
if(href_list["change_track"])
for(var/datum/track/T in tracks)
if(T.title == href_list["title"])
current_track = T
StartPlaying()
break
var/datum/track/T = locate(href_list["change_track"]) in tracks
if(istype(T))
current_track = T
StartPlaying()
else if(href_list["loopmode"])
var/newval = text2num(href_list["loopmode"])
loop_mode = sanitize_inlist(newval, list(JUKEMODE_NEXT, JUKEMODE_RANDOM, JUKEMODE_REPEAT_SONG, JUKEMODE_PLAY_ONCE), loop_mode)
else if(href_list["volume"])
var/newval = input("Choose Jukebox volume (0-100%)", "Jukebox volume", round(volume * 100.0))
newval = sanitize_integer(text2num(newval), min = 0, max = 100, default = volume * 100.0)
volume = newval / 100.0
update_music() // To broadcast volume change without restarting song
else if(href_list["stop"])
StopPlaying()
else if(href_list["play"])
@@ -162,36 +217,39 @@ datum/track/New(var/title_name, var/audio)
return 1
/obj/machinery/media/jukebox/interact(mob/user)
if(stat & (NOPOWER|BROKEN))
if(inoperable())
usr << "\The [src] doesn't appear to function."
return
ui_interact(user)
/obj/machinery/media/jukebox/ui_interact(mob/user, ui_key = "jukebox", var/datum/nanoui/ui = null, var/force_open = 1)
var/title = "RetroBox - Space Style"
var/data[0]
if(!(stat & (NOPOWER|BROKEN)))
data["current_track"] = current_track != null ? current_track.title : ""
if(operable())
data["playing"] = playing
data["hacked"] = hacked
data["max_queue_len"] = max_queue_len
data["media_start_time"] = media_start_time
data["loop_mode"] = loop_mode
data["volume"] = volume
if(current_track)
data["current_track_ref"] = "\ref[current_track]" // Convenient shortcut
data["current_track"] = current_track.toNanoList()
data["percent"] = playing ? min(100, round(world.time - media_start_time) / current_track.duration) : 0;
var/list/nano_tracks = new
for(var/datum/track/T in tracks)
nano_tracks[++nano_tracks.len] = list("track" = T.title)
nano_tracks[++nano_tracks.len] = T.toNanoList()
data["tracks"] = nano_tracks
// update the ui if it exists, returns null if no ui is passed/found
ui = nanomanager.try_update_ui(user, src, ui_key, ui, data, force_open)
if (!ui)
// the ui does not exist, so we'll create a new() one
// for a list of parameters and their descriptions see the code docs in \code\modules\nano\nanoui.dm
ui = new(user, src, ui_key, "jukebox.tmpl", title, 450, 600)
// when the ui is first opened this is the data it will use
ui.set_initial_data(data)
// open the new ui window
ui.open()
ui.set_auto_update(playing)
/obj/machinery/media/jukebox/attack_ai(mob/user as mob)
return src.attack_hand(user)
@@ -212,24 +270,6 @@ datum/track/New(var/title_name, var/audio)
new /obj/effect/decal/cleanable/blood/oil(src.loc)
qdel(src)
/obj/machinery/media/jukebox/attackby(obj/item/W as obj, mob/user as mob)
src.add_fingerprint(user)
if(default_deconstruction_screwdriver(user, W))
return
if(default_deconstruction_crowbar(user, W))
return
if(istype(W, /obj/item/weapon/wrench))
if(playing)
StopPlaying()
user.visible_message("<span class='warning'>[user] has [anchored ? "un" : ""]secured \the [src].</span>", "<span class='notice'>You [anchored ? "un" : ""]secure \the [src].</span>")
anchored = !anchored
playsound(src.loc, 'sound/items/Ratchet.ogg', 50, 1)
power_change()
update_icon()
return
return ..()
/obj/machinery/media/jukebox/emag_act(var/remaining_charges, var/mob/user)
if(!emagged)
emagged = 1
@@ -239,37 +279,19 @@ datum/track/New(var/title_name, var/audio)
return 1
/obj/machinery/media/jukebox/proc/StopPlaying()
var/area/main_area = get_area(src)
// Always kill the current sound
for(var/mob/living/M in mobs_in_area(main_area))
M << sound(null, channel = 1)
main_area.forced_ambience = null
playing = 0
update_use_power(1)
update_icon()
start_stop_song()
/obj/machinery/media/jukebox/proc/StartPlaying()
StopPlaying()
if(!current_track)
return
var/area/main_area = get_area(src)
if(freq)
var/sound/new_song = sound(current_track.sound, channel = 1, repeat = 1, volume = 25)
new_song.frequency = freq
main_area.forced_ambience = list(new_song)
else
main_area.forced_ambience = list(current_track.sound)
for(var/mob/living/M in mobs_in_area(main_area))
if(M.mind)
main_area.play_ambience(M)
playing = 1
update_use_power(2)
update_icon()
start_stop_song()
updateDialog()
// Advance to the next track - Don't start playing it unless we were already playing
/obj/machinery/media/jukebox/proc/NextTrack()
@@ -278,7 +300,7 @@ datum/track/New(var/title_name, var/audio)
var/newTrackIndex = (curTrackIndex % tracks.len) + 1 // Loop back around if past end
current_track = tracks[newTrackIndex]
if(playing)
StartPlaying()
start_stop_song()
updateDialog()
// Advance to the next track - Don't start playing it unless we were already playing
@@ -288,5 +310,5 @@ datum/track/New(var/title_name, var/audio)
var/newTrackIndex = curTrackIndex == 1 ? tracks.len : curTrackIndex - 1
current_track = tracks[newTrackIndex]
if(playing)
StartPlaying()
start_stop_song()
updateDialog()

View File

@@ -1,45 +0,0 @@
//
// VOREStation Custom - Configurable Jukebox!
//
/datum/track
var/secret = 0 // Whether or not this is a SECRET TRACK OOOOOH
// On initialization, copy our tracks from the global list
/obj/machinery/media/jukebox/initialize()
..()
if(all_jukebox_tracks.len)
tracks.Cut()
secret_tracks.Cut()
for(var/datum/track/T in all_jukebox_tracks)
if(T.secret)
secret_tracks += T
else
tracks += T
return
// 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.txt"
if(!fexists(jukebox_track_file))
warning("File not found: [jukebox_track_file]")
return
// Helpful regex that ignores comments and parses our file format
var/regex/lineSplitter = regex("^(?!#)(.+?)\\|(.+?)\\|(.+)$")
var/list/Lines = file2list(jukebox_track_file)
for(var/t in Lines)
if(!t) continue
if(!lineSplitter.Find(t)) continue
var/file = trim(lineSplitter.group[1])
var/title = trim(lineSplitter.group[2])
var/isSecret = text2num(trim(lineSplitter.group[3]))
if(!fexists(file))
warning("In [jukebox_track_file], sound file file not found: [file]")
continue
var/datum/track/T = new(title, file(file))
T.secret = isSecret ? 1 : 0
all_jukebox_tracks += T
return 1

View File

@@ -0,0 +1,53 @@
/datum/preferences
var/media_volume = 1
var/media_player = 2 // 0 = VLC, 1 = WMP, 2 = HTML5, 3+ = unassigned
/datum/category_item/player_setup_item/player_global/media
name = "Media"
sort_order = 6
/datum/category_item/player_setup_item/player_global/media/load_preferences(var/savefile/S)
S["media_volume"] >> pref.media_volume
S["media_player"] >> pref.media_player
/datum/category_item/player_setup_item/player_global/media/save_preferences(var/savefile/S)
S["media_volume"] << pref.media_volume
S["media_player"] << pref.media_player
/datum/category_item/player_setup_item/player_global/media/sanitize_preferences()
pref.media_volume = isnum(pref.media_volume) ? Clamp(pref.media_volume, 0, 1) : initial(pref.media_volume)
pref.media_player = sanitize_inlist(pref.media_player, list(0, 1, 2), initial(pref.media_player))
/datum/category_item/player_setup_item/player_global/media/content(var/mob/user)
. += "<b>Jukebox Volume:</b>"
. += "<a href='?src=\ref[src];change_media_volume=1'><b>[round(pref.media_volume * 100)]%</b></a><br>"
. += "<b>Media Player Type:</b> Depending on you operating system, one of these might work better. "
. += "Use HTML5 if it works for you. If neither HTML5 nor WMP work, you'll have to fall back to using VLC, "
. += "but this requires you have the VLC client installed on your comptuer."
. += "Try the others if you want but you'll probably just get no music.<br>"
. += (pref.media_player == 2) ? "<span class='linkOn'><b>HTML5</b></span> " : "<a href='?src=\ref[src];set_media_player=2'>HTML5</a> "
. += (pref.media_player == 1) ? "<span class='linkOn'><b>WMP</b></span> " : "<a href='?src=\ref[src];set_media_player=1'>WMP</a> "
. += (pref.media_player == 0) ? "<span class='linkOn'><b>VLC</b></span> " : "<a href='?src=\ref[src];set_media_player=0'>VLC</a> "
. += "<br>"
/datum/category_item/player_setup_item/player_global/media/OnTopic(var/href, var/list/href_list, var/mob/user)
if(href_list["change_media_volume"])
if(CanUseTopic(user))
var/value = input("Choose your Jukebox volume (0-100%)", "Jukebox volume", round(pref.media_volume * 100))
if(isnum(value))
value = Clamp(value, 0, 100)
pref.media_volume = value/100.0
if(user.client && user.client.media)
user.client.media.update_volume(pref.media_volume)
return TOPIC_REFRESH
else if(href_list["set_media_player"])
if(CanUseTopic(user))
var/newval = sanitize_inlist(text2num(href_list["set_media_player"]), list(0, 1, 2), pref.media_player)
if(newval != pref.media_player)
pref.media_player = newval
if(user.client && user.client.media)
user.client.media.open()
spawn(10)
user.update_music()
return TOPIC_REFRESH
return ..()

View File

@@ -74,6 +74,16 @@ var/list/_client_preferences_by_type
preference_mob << sound(null, repeat = 0, wait = 0, volume = 0, channel = 1)
preference_mob << sound(null, repeat = 0, wait = 0, volume = 0, channel = 2)
/datum/client_preference/play_jukebox
description ="Play jukebox music"
key = "SOUND_JUKEBOX"
/datum/client_preference/play_jukebox/toggled(var/mob/preference_mob, var/enabled)
if(!enabled)
preference_mob.stop_all_music()
else
preference_mob.update_music()
/datum/client_preference/ghost_ears
description ="Ghost ears"
key = "CHAT_GHOSTEARS"

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)

View File

@@ -2282,6 +2282,32 @@ window "rpane"
is-checked = false
group = "rpanemode"
button-type = pushbox
elem "mediapanel"
type = BROWSER
pos = 392,25
size = 1x1
anchor1 = none
anchor2 = none
font-family = ""
font-size = 0
font-style = ""
text-color = #000000
background-color = none
is-visible = true
is-disabled = false
is-transparent = false
is-default = false
border = none
drop-zone = false
right-click = false
saved-params = ""
on-size = ""
show-history = false
show-url = false
auto-format = true
use-title = false
on-show = ""
on-hide = ""
window "browserwindow"
elem "browserwindow"

View File

@@ -3,17 +3,47 @@ Title: Jukebox UI
Used In File(s): \code\game\machinery\jukebox.dm
-->
<H3><span class="white">Current track:</span> <span class="average">{{:data.current_track}}</span></H3>
<div>
{{:helper.link('Play' , 'play', {'play' : 1}, data.playing == 1 ? 'disabled' : null, null)}}
{{:helper.link('Stop' , 'stop', {'stop' : 1}, data.playing == 0 ? 'disabled' : null, null)}}
<div class='statusDisplay'>
<div class="item">
<div class="itemLabel">Currently Playing:</div>
{{if data.playing && data.current_track }}
<div class="itemContent"><b>{{:data.current_track.title}}</b> by <i>{{:data.current_track.artist ? data.current_track.artist : "Unknown"}}</i></div>
{{else}}
<div class="itemContent">Stopped</div>
{{/if}}
</div>
<div class="item">
<div class="itemContent">
{{:helper.displayBar(data.percent, 0, 1, 'good')}}
</div>
<div class="itemLabel">
{{:helper.link('Play' , 'play', {'play' : 1}, data.playing == 1 ? 'disabled' : null, null)}}
{{:helper.link('Stop' , 'stop', {'stop' : 1}, data.playing == 0 ? 'disabled' : null, null)}}
</div>
</div>
<div class="item">
<div class="itemLabel">{{:helper.link('Volume', 'volume-on', {'volume' : 1})}}</div>
<div class="itemContent">
{{:helper.displayBar(data.volume, 0, 1, (data.volume < .25) ? 'bad' : (data.volume < .75) ? 'average' : 'good')}}
</div>
</div>
</div>
<div class="item">
<div class="itemLabel">Loop Mode:</div>
<div class="itemContent">
{{:helper.link('Next', 'triangle-1-e', {'loopmode' : 1}, data.loop_mode == 1 ? 'disabled' : null, null)}}
{{:helper.link('Random', 'shuffle', {'loopmode' : 2}, data.loop_mode == 2 ? 'disabled' : null, null)}}
{{:helper.link('Repeat', 'arrowrefresh-1-w', {'loopmode' : 3}, data.loop_mode == 3 ? 'disabled' : null, null)}}
{{:helper.link('Once', 'arrowstop-1-e', {'loopmode' : 4}, data.loop_mode == 4 ? 'disabled' : null, null)}}
</div>
</div>
<H3><span class="white">Available tracks:</span></H3>
<div class="itemContent">
{{for data.tracks}}
<div class="item">
{{:helper.link( value.track, 'gear', {'change_track' : 1, 'title' : value.track}, value.track == data.current_track ? 'disabled' : null, null)}}
{{:helper.link(value.title, 'gear', {'change_track' : value.ref}, data.current_track_ref == value.ref ? 'disabled' : null, null)}}
</div>
{{/for}}
</div>
</div>

View File

@@ -565,7 +565,6 @@
#include "code\game\machinery\igniter.dm"
#include "code\game\machinery\iv_drip.dm"
#include "code\game\machinery\jukebox.dm"
#include "code\game\machinery\jukebox_vr.dm"
#include "code\game\machinery\lightswitch.dm"
#include "code\game\machinery\machinery.dm"
#include "code\game\machinery\magnet.dm"
@@ -1241,6 +1240,7 @@
#include "code\modules\client\preference_setup\global\03_pai.dm"
#include "code\modules\client\preference_setup\global\04_communicators.dm"
#include "code\modules\client\preference_setup\global\05_ooc.dm"
#include "code\modules\client\preference_setup\global\06_media.dm"
#include "code\modules\client\preference_setup\global\setting_datums.dm"
#include "code\modules\client\preference_setup\loadout\gear_tweaks.dm"
#include "code\modules\client\preference_setup\loadout\loadout.dm"
@@ -1550,6 +1550,12 @@
#include "code\modules\materials\material_synth.dm"
#include "code\modules\materials\materials.dm"
#include "code\modules\materials\materials_vr.dm"
#include "code\modules\media\media_machinery.dm"
#include "code\modules\media\media_player_html5.dm"
#include "code\modules\media\media_player_vlc.dm"
#include "code\modules\media\media_player_wmp.dm"
#include "code\modules\media\media_tracks.dm"
#include "code\modules\media\mediamanager.dm"
#include "code\modules\metric\activity.dm"
#include "code\modules\metric\department.dm"
#include "code\modules\metric\metric.dm"