mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 10:12:45 +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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
53
code/modules/client/preference_setup/global/06_media.dm
Normal file
53
code/modules/client/preference_setup/global/06_media.dm
Normal 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 ..()
|
||||
@@ -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"
|
||||
|
||||
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)
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user