good progress
This commit is contained in:
parent
90ea2174db
commit
fb7ade79cb
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
Populate player name (from most used one) after database is fully populated:
|
||||
```SQL
|
||||
UPDATE `player` SET `name`=(
|
||||
SELECT `name`
|
||||
FROM `player_names`
|
||||
WHERE `player_names`.guid = `player`.guid
|
||||
GROUP BY guid, name
|
||||
ORDER BY MAX(`time`) DESC LIMIT 1
|
||||
);
|
||||
```
|
@ -1,5 +1,5 @@
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import Flask, g, has_app_context, request
|
||||
from .extensions import default_config
|
||||
@ -17,6 +17,21 @@ def register_jinja2_filters(app):
|
||||
def to_json(value):
|
||||
return orjson.dumps(value).decode('utf-8')
|
||||
|
||||
@app.template_filter('to_duration')
|
||||
def to_duration(seconds):
|
||||
out = ''
|
||||
(days, remainder) = divmod(seconds, 86400)
|
||||
(hours, remainder) = divmod(remainder, 3600)
|
||||
(minutes, seconds) = divmod(remainder, 60)
|
||||
if days:
|
||||
out += f'{days:.0f}d '
|
||||
if hours:
|
||||
out += f'{hours:.0f}h '
|
||||
if minutes:
|
||||
out += f'{minutes:.0f}m '
|
||||
out += f'{seconds:.0f}s'
|
||||
return out
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
with app.app_context():
|
||||
|
@ -44,15 +44,19 @@ class Player(db.Model):
|
||||
|
||||
|
||||
class PlayerNames(db.Model):
|
||||
guid = db.Column(db.String(32), primary_key=True)
|
||||
guid = db.Column(db.String(32), db.ForeignKey('player.guid'), primary_key=True)
|
||||
name = db.Column(db.String(32), primary_key=True)
|
||||
time = db.Column(db.Float)
|
||||
|
||||
player = db.relationship('Player', backref='names', foreign_keys=[guid])
|
||||
|
||||
|
||||
class PlayerSprays(db.Model):
|
||||
guid = db.Column(db.String(32), primary_key=True)
|
||||
guid = db.Column(db.String(32), db.ForeignKey('player.guid'), primary_key=True)
|
||||
spray = db.Column(db.String(32), primary_key=True)
|
||||
|
||||
player = db.relationship('Player', backref='sprays', foreign_keys=[guid])
|
||||
|
||||
|
||||
class PlayerSession(db.Model):
|
||||
player_guid = db.Column(db.String(32), db.ForeignKey('player.guid'), primary_key=True)
|
||||
@ -63,6 +67,9 @@ class PlayerSession(db.Model):
|
||||
kills = db.Column(db.Integer)
|
||||
voicetime = db.Column(db.Float)
|
||||
|
||||
session = db.relationship('Session', foreign_keys=[session_id])
|
||||
player = db.relationship('Player', backref='sessions', foreign_keys=[player_guid])
|
||||
|
||||
|
||||
class Chat(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -1,8 +1,7 @@
|
||||
main { display: flex; flex-wrap: nowrap; height: 100vh; height: -webkit-fill-available; max-height: 100vh; overflow-x: auto; overflow-y: hidden; }
|
||||
|
||||
.btn-group-xs > .btn, .btn-xs { padding: .25rem .4rem; font-size: .875rem; line-height: .5; border-radius: .2rem; }
|
||||
|
||||
.playlist { margin: 2em 0; }
|
||||
|
||||
.playlist .playlist-time-scale { height: 30px; }
|
||||
|
||||
.playlist .playlist-tracks { background: #e0eff1; }
|
||||
@ -31,10 +30,12 @@
|
||||
|
||||
.playlist .controls { background: white; text-align: center; border: 1px solid black; border-radius: 0.2rem; }
|
||||
|
||||
.playlist .controls .track-header { overflow: hidden; color: black; height: 26px; display: flex; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-bottom: -10px; }
|
||||
.playlist .controls .track-header { overflow: hidden; color: black; height: 18px; display: flex; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-bottom: -10px; }
|
||||
|
||||
.playlist .controls .track-header button { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
.playlist .controls .track-header span { margin-top: -8px; }
|
||||
|
||||
.playlist .controls input[type="range"] { display: inline-block; width: 90%; }
|
||||
|
||||
.playlist .controls .user-info { word-break: break-word; color: black; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-top: -5px; }
|
||||
.playlist .controls .info { word-break: break-word; color: black; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-top: -10px; }
|
||||
|
@ -1,7 +1,7 @@
|
||||
var playlist = WaveformPlaylist.init({
|
||||
container: document.getElementById("playlist"),
|
||||
timescale: true,
|
||||
state: 'cursor',
|
||||
state: 'select',
|
||||
samplesPerPixel: 16384,
|
||||
zoomLevels: [2048, 4096, 8192, 16384],
|
||||
|
||||
@ -12,23 +12,58 @@ var playlist = WaveformPlaylist.init({
|
||||
stereoPan: false,
|
||||
}
|
||||
},
|
||||
waveHeight: 96,
|
||||
waveHeight: 80,
|
||||
});
|
||||
|
||||
function onFinishedLoading() {
|
||||
//initialize the WAV exporter.
|
||||
playlist.initExporter();
|
||||
|
||||
const tracks = playlist.tracks;
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
playlist.collapseTrack(tracks[i], {collapsed: true});
|
||||
}
|
||||
|
||||
const highlight = window.location.hash.split('#').filter(Boolean);
|
||||
for(var i = 0; i < highlight.length; i++) {
|
||||
const guid = highlight[i];
|
||||
|
||||
for (var j = 0; j < tracks.length; j++) {
|
||||
if (tracks[j].name == guid) {
|
||||
tracks[j].setWaveOutlineColor('#d1e7dd');
|
||||
}
|
||||
}
|
||||
|
||||
for (var j = 0; j < g_chats.length; j++) {
|
||||
if (g_chats[j].player_guid == guid) {
|
||||
chatRows[j].classList.add("table-active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playlist.drawRequest();
|
||||
}
|
||||
|
||||
function collapseTrack(guid, collapse) {
|
||||
const tracks = playlist.tracks;
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
if (guid === null || tracks[i].name == guid) {
|
||||
playlist.collapseTrack(tracks[i], {collapsed: collapse});
|
||||
if (guid != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTrackInfo(guid, info) {
|
||||
var tracks = $(".playlist-tracks").children();
|
||||
const tracks = playlist.tracks;
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
var track = tracks[i].firstChild;
|
||||
var trackGuid = track.firstChild.getElementsByTagName('span')[0].innerText;
|
||||
var userInfoElem = track.lastChild;
|
||||
if (userInfoElem.className != 'user-info') {
|
||||
userInfoElem = document.createElement('label');
|
||||
userInfoElem.className = 'user-info';
|
||||
track.appendChild(userInfoElem);
|
||||
if (guid === null || tracks[i].name == guid) {
|
||||
tracks[i].setInfo(info);
|
||||
if (guid != null) {
|
||||
break;
|
||||
}
|
||||
if (guid === null || trackGuid == guid) {
|
||||
userInfoElem.innerText = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,25 +91,144 @@ function clockFormat(seconds, decimals) {
|
||||
return result;
|
||||
}
|
||||
|
||||
var lastChunkIdx = 0;
|
||||
var lastUpdateTime = 0;
|
||||
function updateTime(time) {
|
||||
$time.html(clockFormat(time, 3));
|
||||
audioPos = time;
|
||||
|
||||
if (time < lastUpdateTime) {
|
||||
lastChunkIdx = 0;
|
||||
}
|
||||
|
||||
var tick = time / g_session.tickinterval;
|
||||
var silenceTicks = 0;
|
||||
for (var i = 0; i < g_session.silence_chunks.length; i++) {
|
||||
var chunk = g_session.silence_chunks[i];
|
||||
for (; lastChunkIdx < g_session.silence_chunks.length; lastChunkIdx++) {
|
||||
var chunk = g_session.silence_chunks[lastChunkIdx];
|
||||
if (tick > chunk[0]) {
|
||||
silenceTicks += chunk[1];
|
||||
silenceTicks = chunk[1];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
var tickedTime = (tick + silenceTicks) * g_session.tickinterval;
|
||||
|
||||
tick += silenceTicks;
|
||||
var tickedTime = tick * g_session.tickinterval;
|
||||
$time_.html(clockFormat(tickedTime, 3));
|
||||
|
||||
lastUpdateTime = time;
|
||||
if (lastChunkIdx > 0) {
|
||||
lastChunkIdx -= 1;
|
||||
}
|
||||
|
||||
onTick(tick, tickedTime);
|
||||
}
|
||||
updateTime(audioPos);
|
||||
|
||||
function onEvent(idx, event) {
|
||||
var update = 0;
|
||||
if (event.event == "player_connect") {
|
||||
collapseTrack(event.player_guid, false);
|
||||
updateTrackInfo(event.player_guid, event.data.name);
|
||||
update += 1;
|
||||
}
|
||||
else if (event.event == "player_disconnect") {
|
||||
collapseTrack(event.player_guid, true);
|
||||
update += 1;
|
||||
}
|
||||
else if (event.event == "player_changename") {
|
||||
updateTrackInfo(event.player_guid, event.data.newname);
|
||||
update += 1;
|
||||
}
|
||||
return update;
|
||||
}
|
||||
|
||||
updateTime(audioPos);
|
||||
var chatBox = $("div#chat");
|
||||
var chatRows = $("div#chat>table>tbody").children();
|
||||
var lastPrimaryRow = undefined;
|
||||
function onChat(idx, chat) {
|
||||
if (idx == lastPrimaryRow) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lastPrimaryRow != undefined) {
|
||||
chatRows[lastPrimaryRow].classList.remove("table-primary");
|
||||
}
|
||||
|
||||
chatRows[idx].classList.add("table-primary");
|
||||
if (autoScrollChat) {
|
||||
chatRows[idx].scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
lastPrimaryRow = idx;
|
||||
return 1;
|
||||
}
|
||||
|
||||
var lastTick = undefined;
|
||||
var lastChatIdx = 0;
|
||||
var lastEventIdx = 0;
|
||||
function onTick(tick, time) {
|
||||
var update = 0;
|
||||
if (tick == lastTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tick < lastTick) {
|
||||
lastChatIdx = 0;
|
||||
lastEventIdx = 0;
|
||||
}
|
||||
|
||||
for (; lastEventIdx < g_events.length; lastEventIdx++) {
|
||||
const event = g_events[lastEventIdx];
|
||||
if (tick > event.tick) {
|
||||
update += onEvent(lastEventIdx, event);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastEventIdx > 0) {
|
||||
lastEventIdx -= 1;
|
||||
}
|
||||
|
||||
for (; lastChatIdx < g_chats.length; lastChatIdx++) {
|
||||
const chat = g_chats[lastChatIdx];
|
||||
if (tick < chat.tick) {
|
||||
if (lastChatIdx > 0) {
|
||||
lastChatIdx -= 1;
|
||||
}
|
||||
update += onChat(lastChatIdx, chat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lastTick = tick;
|
||||
|
||||
if (update) {
|
||||
playlist.drawRequest();
|
||||
}
|
||||
}
|
||||
|
||||
function gameTimeToAudio(tick) {
|
||||
tick += 1;
|
||||
var silenceTicks = 0;
|
||||
for (var i = 0; i < g_session.silence_chunks.length; i++) {
|
||||
const chunk = g_session.silence_chunks[i];
|
||||
if ((tick - chunk[1]) > chunk[0]) {
|
||||
silenceTicks = chunk[1];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (tick - silenceTicks) * g_session.tickinterval;
|
||||
}
|
||||
|
||||
function jumpToGameTick(tick) {
|
||||
var audioTime = gameTimeToAudio(tick);
|
||||
playlist.seek(audioTime);
|
||||
playlist.drawRequest();
|
||||
updateTime(audioTime);
|
||||
}
|
||||
|
||||
$container.on("click", ".btn-play", function() {
|
||||
ee.emit("play");
|
||||
@ -100,7 +254,7 @@ $container.on("click", ".btn-fast-forward", function() {
|
||||
ee.emit("fastforward");
|
||||
});
|
||||
|
||||
//zoom buttons
|
||||
// zoom buttons
|
||||
$container.on("click", ".btn-zoom-in", function() {
|
||||
ee.emit("zoomin");
|
||||
});
|
||||
@ -109,21 +263,102 @@ $container.on("click", ".btn-zoom-out", function() {
|
||||
ee.emit("zoomout");
|
||||
});
|
||||
|
||||
// download
|
||||
var downloadUrl = undefined;
|
||||
var downloadName = undefined;
|
||||
$container.on("click", ".btn-download", function () {
|
||||
if (downloadName) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadName = g_session.demoname;
|
||||
if (playlist.isSegmentSelection()) {
|
||||
const segment = playlist.getTimeSelection();
|
||||
downloadName += "-" + clockFormat(segment.start).replaceAll(':', '-') + "_" + clockFormat(segment.end).replaceAll(':', '-');
|
||||
}
|
||||
downloadName += ".wav";
|
||||
|
||||
ee.emit('startaudiorendering', 'wav');
|
||||
});
|
||||
|
||||
ee.on('audiorenderingfinished', function (type, data) {
|
||||
if (type != 'wav') {
|
||||
return;
|
||||
}
|
||||
if (downloadUrl) {
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
downloadUrl = window.URL.createObjectURL(data);
|
||||
|
||||
const tempLink = document.createElement('a');
|
||||
tempLink.style.display = 'none';
|
||||
tempLink.href = downloadUrl;
|
||||
tempLink.setAttribute('download', downloadName);
|
||||
document.body.appendChild(tempLink);
|
||||
tempLink.click();
|
||||
document.body.removeChild(tempLink);
|
||||
|
||||
downloadName = undefined;
|
||||
});
|
||||
|
||||
|
||||
$container.on("input change", ".master-gain", function(e){
|
||||
ee.emit("mastervolumechange", e.target.value);
|
||||
});
|
||||
$container.find(".master-gain").change();
|
||||
|
||||
$container.on("change", ".automatic-scroll", function(e){
|
||||
ee.emit("automaticscroll", $(e.target).is(':checked'));
|
||||
var autoScrollVoice = false;
|
||||
$container.on("change", "#autoscroll_voice", function(e){
|
||||
autoScrollVoice = $(e.target).is(':checked');
|
||||
ee.emit("automaticscroll", autoScrollVoice);
|
||||
});
|
||||
$container.find(".automatic-scroll").change();
|
||||
$container.find("#autoscroll_voice").change();
|
||||
|
||||
var autoScrollChat = false;
|
||||
$container.on("change", "#autoscroll_chat", function(e){
|
||||
autoScrollChat = $(e.target).is(':checked');
|
||||
});
|
||||
$container.find("#autoscroll_chat").change();
|
||||
|
||||
ee.on("timeupdate", updateTime);
|
||||
|
||||
|
||||
function onFinishedLoading() {
|
||||
updateTrackInfo(null, "name");
|
||||
function getParent(el) {
|
||||
var parent = el.parentNode;
|
||||
|
||||
if (parent === document) {
|
||||
return document;
|
||||
} else if (parent.offsetHeight < parent.scrollHeight || parent.offsetWidth < parent.scrollWidth) {
|
||||
return parent;
|
||||
} else {
|
||||
return getParent(parent);
|
||||
}
|
||||
}
|
||||
if (!Element.prototype.scrollIntoViewIfNeeded) {
|
||||
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
|
||||
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
|
||||
|
||||
var parent = getParent(this),
|
||||
parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
|
||||
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
|
||||
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
|
||||
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
|
||||
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
|
||||
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
|
||||
alignWithTop = overTop && !overBottom;
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
}
|
||||
|
||||
if ((overLeft || overRight) && centerIfNeeded) {
|
||||
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
|
||||
}
|
||||
|
||||
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||
this.scrollIntoView(alignWithTop);
|
||||
}
|
||||
};
|
||||
}
|
@ -106,7 +106,8 @@ var WaveformPlaylist =
|
||||
volume: true,
|
||||
stereoPan: true,
|
||||
collapse: true,
|
||||
remove: true
|
||||
remove: true,
|
||||
info: true
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
@ -3465,6 +3466,7 @@ var WaveformPlaylist =
|
||||
var tracks = audioBuffers.map(function (audioBuffer, index) {
|
||||
var info = trackList[index];
|
||||
var name = info.name || 'Untitled';
|
||||
var infostr = info.info || undefined;
|
||||
var start = info.start || 0;
|
||||
var states = info.states || {};
|
||||
var fadeIn = info.fadeIn;
|
||||
@ -3487,6 +3489,7 @@ var WaveformPlaylist =
|
||||
track.src = info.src;
|
||||
track.setBuffer(audioBuffer);
|
||||
track.setName(name);
|
||||
track.setInfo(infostr);
|
||||
track.setEventEmitter(_this3.ee);
|
||||
track.setEnabledStates(states);
|
||||
track.setCues(cueIn, cueOut);
|
||||
@ -3586,15 +3589,24 @@ var WaveformPlaylist =
|
||||
if (this.isRendering) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRendering = true;
|
||||
this.offlineAudioContext = new OfflineAudioContext(2, 44100 * this.duration, 44100);
|
||||
|
||||
var duration = this.duration;
|
||||
var startTime = 0;
|
||||
var endTime = 0;
|
||||
if (this.isSegmentSelection()) {
|
||||
var segment = this.getTimeSelection();
|
||||
startTime = segment.start;
|
||||
endTime = segment.end;
|
||||
duration = endTime - startTime;
|
||||
}
|
||||
|
||||
this.offlineAudioContext = new OfflineAudioContext(1, 44100 * duration, 44100);
|
||||
var currentTime = this.offlineAudioContext.currentTime;
|
||||
|
||||
this.tracks.forEach(function (track) {
|
||||
track.setOfflinePlayout(new _Playout2.default(_this4.offlineAudioContext, track.buffer));
|
||||
track.schedulePlay(currentTime, 0, 0, {
|
||||
track.schedulePlay(currentTime, startTime, endTime, {
|
||||
shouldPlay: _this4.shouldTrackPlay(track),
|
||||
masterGain: 1,
|
||||
isOffline: true
|
||||
@ -3615,7 +3627,8 @@ var WaveformPlaylist =
|
||||
_this4.exportWorker.postMessage({
|
||||
command: 'init',
|
||||
config: {
|
||||
sampleRate: 44100
|
||||
sampleRate: 44100,
|
||||
stereo: false
|
||||
}
|
||||
});
|
||||
|
||||
@ -3633,7 +3646,7 @@ var WaveformPlaylist =
|
||||
// send the channel data from our buffer to the worker
|
||||
_this4.exportWorker.postMessage({
|
||||
command: 'record',
|
||||
buffer: [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)]
|
||||
buffer: [audioBuffer.getChannelData(0)]
|
||||
});
|
||||
|
||||
// ask the worker for a WAV
|
||||
@ -5905,7 +5918,7 @@ var WaveformPlaylist =
|
||||
|
||||
if (audioData.byteLength > 16) {
|
||||
var view = new DataView(audioData);
|
||||
var wanted = "DEMOPUSHEADER_V1";
|
||||
var wanted = "DEMOPUSHEADER_V2";
|
||||
var success = true;
|
||||
for (var i = 0, n = 16; i < n; i++) {
|
||||
var c = view.getUint8(i);
|
||||
@ -5942,29 +5955,41 @@ var WaveformPlaylist =
|
||||
var _this2 = this;
|
||||
|
||||
this.setStateChange(STATE_DECODING);
|
||||
|
||||
var parsed = [];
|
||||
var sampleRate = 0;
|
||||
var numSamples = 0;
|
||||
var channels = 1;
|
||||
var promises = [];
|
||||
|
||||
var view = new DataView(demopusData);
|
||||
var ofs = 16; // skip header
|
||||
|
||||
while (ofs < demopusData.byteLength) {
|
||||
var header = view.getUint8(ofs);
|
||||
ofs += 1;
|
||||
|
||||
if (header == 0x02) {
|
||||
// opus
|
||||
var dataLen = Number(view.getBigUint64(ofs, true));
|
||||
var channels = 1;
|
||||
var sampleRate = view.getUint32(ofs, true);
|
||||
ofs += 4;
|
||||
var numSamples = Number(view.getBigUint64(ofs, true));
|
||||
ofs += 8;
|
||||
|
||||
// output sample rate != input sample rate
|
||||
numSamples *= this.ac.sampleRate / sampleRate;
|
||||
var audioBuffer = this.ac.createBuffer(channels, numSamples, this.ac.sampleRate);
|
||||
|
||||
while (ofs < demopusData.byteLength) {
|
||||
var samplesOfs = Number(view.getBigUint64(ofs, true));
|
||||
ofs += 8;
|
||||
samplesOfs *= this.ac.sampleRate / sampleRate;
|
||||
|
||||
if (ofs >= demopusData.byteLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
var dataLen = view.getUint32(ofs, true);
|
||||
ofs += 4;
|
||||
|
||||
var opusData = demopusData.slice(ofs, ofs + dataLen);
|
||||
ofs += dataLen;
|
||||
|
||||
var promise = this.ac.decodeAudioData(opusData, function (audioBuffer) {
|
||||
return audioBuffer;
|
||||
}, function (err) {
|
||||
var promise = this.ac.decodeAudioData(opusData, function (decoded) {
|
||||
var buf = decoded.getChannelData(0);
|
||||
audioBuffer.copyToChannel(buf, 0, this);
|
||||
return decoded.length;
|
||||
}.bind(samplesOfs), function (err) {
|
||||
if (err === null) {
|
||||
// Safari issues with null error
|
||||
return Error('MediaDecodeAudioDataUnknownContentType');
|
||||
@ -5973,43 +5998,11 @@ var WaveformPlaylist =
|
||||
}
|
||||
});
|
||||
|
||||
parsed.push(promise);
|
||||
} else if (header == 0x03) {
|
||||
// silence
|
||||
var samples = Number(view.getBigUint64(ofs, true));
|
||||
ofs += 8;
|
||||
parsed.push(samples);
|
||||
} else if (header == 0x01) {
|
||||
// info
|
||||
sampleRate = view.getUint32(ofs, true);
|
||||
ofs += 4;
|
||||
numSamples = Number(view.getBigUint64(ofs, true));
|
||||
ofs += 8;
|
||||
} else if (header == 0x04) {
|
||||
// done
|
||||
break;
|
||||
}
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
// output sample rate != input sample rate
|
||||
numSamples *= _this2.ac.sampleRate / sampleRate;
|
||||
var audioBuffer = _this2.ac.createBuffer(channels, numSamples, _this2.ac.sampleRate);
|
||||
|
||||
return Promise.all(parsed).then(function (result) {
|
||||
var curSamples = 0;
|
||||
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var elem = result[i];
|
||||
if (typeof elem == "number") {
|
||||
curSamples += elem * (_this2.ac.sampleRate / sampleRate);
|
||||
} else {
|
||||
var buf = elem.getChannelData(0);
|
||||
audioBuffer.copyToChannel(buf, 0, curSamples);
|
||||
curSamples += elem.length;
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(promises).then(function (result) {
|
||||
_this2.setStateChange(STATE_FINISHED);
|
||||
resolve(audioBuffer);
|
||||
});
|
||||
@ -6505,6 +6498,7 @@ var WaveformPlaylist =
|
||||
_classCallCheck(this, _class);
|
||||
|
||||
this.name = 'Untitled';
|
||||
this.info = undefined;
|
||||
this.customClass = undefined;
|
||||
this.waveOutlineColor = undefined;
|
||||
this.gain = 1;
|
||||
@ -6532,6 +6526,11 @@ var WaveformPlaylist =
|
||||
value: function setName(name) {
|
||||
this.name = name;
|
||||
}
|
||||
}, {
|
||||
key: 'setInfo',
|
||||
value: function setInfo(info) {
|
||||
this.info = info;
|
||||
}
|
||||
}, {
|
||||
key: 'setCustomClass',
|
||||
value: function setCustomClass(className) {
|
||||
@ -6980,6 +6979,10 @@ var WaveformPlaylist =
|
||||
}
|
||||
})]));
|
||||
}
|
||||
|
||||
if (widgets.info) {
|
||||
controls.push((0, _h2.default)('label.info', [this.info]));
|
||||
}
|
||||
}
|
||||
|
||||
return (0, _h2.default)('div.controls', {
|
||||
@ -7134,6 +7137,7 @@ var WaveformPlaylist =
|
||||
start: this.startTime,
|
||||
end: this.endTime,
|
||||
name: this.name,
|
||||
info: this.info,
|
||||
customClass: this.customClass,
|
||||
cuein: this.cueIn,
|
||||
cueout: this.cueOut,
|
||||
@ -10408,14 +10412,18 @@ var WaveformPlaylist =
|
||||
var recBuffersL = [];
|
||||
var recBuffersR = [];
|
||||
var sampleRate = void 0;
|
||||
var stereo = void 0;
|
||||
|
||||
function init(config) {
|
||||
sampleRate = config.sampleRate;
|
||||
stereo = config.stereo;
|
||||
}
|
||||
|
||||
function record(inputBuffer) {
|
||||
recBuffersL.push(inputBuffer[0]);
|
||||
if (stereo) {
|
||||
recBuffersR.push(inputBuffer[1]);
|
||||
}
|
||||
recLength += inputBuffer[0].length;
|
||||
}
|
||||
|
||||
@ -10434,15 +10442,14 @@ var WaveformPlaylist =
|
||||
}
|
||||
|
||||
function encodeWAV(samples) {
|
||||
var mono = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
|
||||
var numChannels = stereo ? 2 : 1;
|
||||
var buffer = new ArrayBuffer(44 + samples.length * 2);
|
||||
var view = new DataView(buffer);
|
||||
|
||||
/* RIFF identifier */
|
||||
writeString(view, 0, 'RIFF');
|
||||
/* file length */
|
||||
view.setUint32(4, 32 + samples.length * 2, true);
|
||||
view.setUint32(4, 36 + samples.length * 2, true);
|
||||
/* RIFF type */
|
||||
writeString(view, 8, 'WAVE');
|
||||
/* format chunk identifier */
|
||||
@ -10452,13 +10459,13 @@ var WaveformPlaylist =
|
||||
/* sample format (raw) */
|
||||
view.setUint16(20, 1, true);
|
||||
/* channel count */
|
||||
view.setUint16(22, mono ? 1 : 2, true);
|
||||
view.setUint16(22, numChannels, true);
|
||||
/* sample rate */
|
||||
view.setUint32(24, sampleRate, true);
|
||||
/* byte rate (sample rate * block align) */
|
||||
view.setUint32(28, sampleRate * 4, true);
|
||||
/* byte rate (sample rate * channel count * bytes per sample) */
|
||||
view.setUint32(28, sampleRate * numChannels * 2, true);
|
||||
/* block align (channel count * bytes per sample) */
|
||||
view.setUint16(32, 4, true);
|
||||
view.setUint16(32, numChannels * 2, true);
|
||||
/* bits per sample */
|
||||
view.setUint16(34, 16, true);
|
||||
/* data chunk identifier */
|
||||
@ -10500,8 +10507,11 @@ var WaveformPlaylist =
|
||||
|
||||
function exportWAV(type) {
|
||||
var bufferL = mergeBuffers(recBuffersL, recLength);
|
||||
var interleaved = bufferL;
|
||||
if (stereo) {
|
||||
var bufferR = mergeBuffers(recBuffersR, recLength);
|
||||
var interleaved = interleave(bufferL, bufferR);
|
||||
interleaved = interleave(bufferL, bufferR);
|
||||
}
|
||||
var dataview = encodeWAV(interleaved);
|
||||
var audioBlob = new Blob([dataview], { type: type });
|
||||
|
||||
|
1
demweb/static/js/waveform-playlist.var.js.map
Normal file
1
demweb/static/js/waveform-playlist.var.js.map
Normal file
File diff suppressed because one or more lines are too long
172
demweb/templates/player.html
Normal file
172
demweb/templates/player.html
Normal file
@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>
|
||||
{{ player.guid }} - {{ player.name }}
|
||||
</title>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/css/theme.bootstrap_4.min.css"
|
||||
integrity="sha512-2C6AmJKgt4B+bQc08/TwUeFKkq8CsBNlTaNcNgUmsDJSU1Fg+R6azDbho+ZzuxEkJnCjLZQMozSq3y97ZmgwjA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">guid</th>
|
||||
<td>
|
||||
<a href="https://steamcommunity.com/profiles/{{ player.guid }}" target="_blank">
|
||||
{{ player.guid }}
|
||||
</a>
|
||||
</td>
|
||||
<tr>
|
||||
<th scope="row">name</th>
|
||||
<td>{{ player.name }}</td>
|
||||
<tr>
|
||||
<th scope="row">first_seen</th>
|
||||
<td>{{ player.first_seen }}</td>
|
||||
<tr>
|
||||
<th scope="row">last_seen</th>
|
||||
<td>{{ player.last_seen }}</td>
|
||||
<tr>
|
||||
<th scope="row">playtime</th>
|
||||
<td>{{ player.playtime | to_duration }}</td>
|
||||
<tr>
|
||||
<th scope="row">chats</th>
|
||||
<td>{{ player.chats }}</td>
|
||||
<tr>
|
||||
<th scope="row">deaths</th>
|
||||
<td>{{ player.deaths }}</td>
|
||||
<tr>
|
||||
<th scope="row">kills</th>
|
||||
<td>{{ player.kills }}</td>
|
||||
<tr>
|
||||
<th scope="row">voicetime</th>
|
||||
<td>{{ player.voicetime | to_duration }}</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<table class="table table-striped tablesorter" id="playerNamesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col" class="sorter-duration">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for name in player.names %}
|
||||
<tr>
|
||||
<th scope="row">{{ name.name }}</th>
|
||||
<td>{{ (name.time * 0.015) | to_duration }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<table class="table table-striped" id="playerSpraysTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Spray</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for spray in player.sprays %}
|
||||
<tr>
|
||||
<th scope="row">{{ spray.spray }}</th>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3>Sessions</h3>
|
||||
<table class="table table-striped tablesorter" id="playerSessionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" class="sorter-isoDate">Time</th>
|
||||
<th scope="col" class="sorter-duration">Length</th>
|
||||
<th scope="col">Map</th>
|
||||
<th scope="col" class="sorter-duration">Playtime</th>
|
||||
<th scope="col">Chats</th>
|
||||
<th scope="col" class="sorter-duration">Voicetime</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for session in player_sessions %}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<a href="{{ url_for('views.session', session_id=session.session.id) }}#{{ player.guid }}">
|
||||
{{ session.session.id }}
|
||||
</a>
|
||||
</th>
|
||||
<td>{{ session.session.time.isoformat(' ') }}</td>
|
||||
<td>{{ session.session.length | to_duration }}</td>
|
||||
<td>{{ session.session.mapname }}</td>
|
||||
<td>{{ session.playtime | to_duration }}</td>
|
||||
<td>{{ session.chats }}</td>
|
||||
<td>{{ session.voicetime | to_duration }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.min.js"
|
||||
integrity="sha512-qzgd5cYSZcosqpzpn7zF2ZId8f/8CHmFKZ8j7mU4OUXTNRd5g+ZHBPsgKEwoqxCtdQvExE5LprwwPAgoicguNg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/parsers/parser-duration.min.js"
|
||||
integrity="sha512-X7QJLLEO6yg8gSlmgRAP7Ec2qDD+ndnFcd8yagZkkN5b/7bCMbhRQdyJ4SjENUEr+4eBzgwvaFH5yR/bLJZJQA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#playerNamesTable").tablesorter({
|
||||
theme: "bootstrap"
|
||||
});
|
||||
$("#playerSessionsTable").tablesorter({
|
||||
theme: "bootstrap"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -16,35 +16,10 @@
|
||||
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Top navbar</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex">
|
||||
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
|
||||
<main class="container-fluid">
|
||||
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<div class="btn-toolbar" role="toolbar" style="min-width: max-content;">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn-pause btn btn-outline-warning" title="Pause">
|
||||
<i class="fas fa-pause"></i>
|
||||
@ -91,24 +66,63 @@
|
||||
id="master-gain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2">
|
||||
<div style="margin: 6px">
|
||||
<span class="audio-pos" aria-label="Audio position">00:00:00.0</span>
|
||||
<span class="audio-pos font-monospace" aria-label="Audio position">00:00:00.0</span>
|
||||
</div>
|
||||
|
||||
<div style="margin: 6px">
|
||||
<span class="audio-pos-2" aria-label="Audio position">00:00:00.0</span>
|
||||
<span class="audio-pos-2 font-monospace" aria-label="Audio position">00:00:00.0</span>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input automatic-scroll" type="checkbox" id="automatic_scroll" checked>
|
||||
<label class="form-check-label" for="automatic_scroll">Autoscroll</label>
|
||||
<input class="form-check-input" type="checkbox" id="autoscroll_voice" checked>
|
||||
<label class="form-check-label" for="autoscroll_voice">Voice</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<label class="form-check-label" for="autoscroll_chat">Chat</label>
|
||||
<input class="form-check-input" type="checkbox" id="autoscroll_chat" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" title="Download the selection as Wav file" class="btn btn-download btn-outline-primary">
|
||||
<i class="fas fa-download" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto" id="playlist">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="playlist">
|
||||
|
||||
<div class="d-flex flex-column overflow-auto" id="chat">
|
||||
<table class="table table-sm text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">SteamID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for chat in chats %}
|
||||
<tr>
|
||||
<td onclick="jumpToGameTick({{ chat.tick }})">
|
||||
{{ (chat.tick * session.tickinterval) | to_duration }}
|
||||
</td>
|
||||
<td>{{ chat.player_guid }}</td>
|
||||
<td>{{ chat.name }}</td>
|
||||
<td>{{ chat.chat }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
@ -154,7 +168,7 @@ var g_events = [
|
||||
playlist.load([
|
||||
{%- for guid, psess in player_sessions.items() %}
|
||||
{%- if psess.voicetime > 0 %}
|
||||
{src: "/static/css-ze-parsed/{{ session.demoname }}/voice/{{ guid }}.demopus", name: "{{ guid }}"},
|
||||
{src: "/static/css-ze-parsed/{{ session.demoname }}/voice/{{ guid }}.demopus", name: "{{ guid }}", info: "{{ psess.player.name }}"},
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
]).then(function() {
|
||||
|
@ -40,3 +40,16 @@ def session(session_id):
|
||||
chats=chats,
|
||||
events=events
|
||||
)
|
||||
|
||||
@bp.route('/player/<guid>')
|
||||
def player(guid):
|
||||
player = Player.query.filter_by(guid=guid).one()
|
||||
if not player:
|
||||
return '404 Not Found', 404
|
||||
|
||||
player_sessions = PlayerSession.query.filter_by(player_guid=guid).all()
|
||||
|
||||
return render_template('player.html',
|
||||
player=player,
|
||||
player_sessions=player_sessions
|
||||
)
|
||||
|
@ -125,7 +125,7 @@ def parse_demo(path):
|
||||
kills = models.Player.kills + obj['kills'],
|
||||
voicetime = models.Player.voicetime + obj['voicetime'],
|
||||
first_seen = func.least(models.Player.first_seen, starttime),
|
||||
last_seen = func.greatest(models.Player.first_seen, starttime),
|
||||
last_seen = func.greatest(models.Player.last_seen, starttime),
|
||||
))
|
||||
db.session.commit()
|
||||
break
|
||||
@ -225,9 +225,11 @@ def parse_demo(path):
|
||||
eventdata = obj.copy()
|
||||
del eventdata['tick']
|
||||
del eventdata['event']
|
||||
if 'steamid' in eventdata:
|
||||
del eventdata['steamid']
|
||||
|
||||
event = models.Event(
|
||||
player_guid=guid,
|
||||
player_guid=obj['steamid'] if 'steamid' in obj else None,
|
||||
session_id=session.id,
|
||||
tick=obj['tick'],
|
||||
time=starttime + timedelta(seconds=obj['tick'] * tickinterval),
|
||||
|
@ -12,6 +12,7 @@ orjson==3.5.2
|
||||
python-dotenv==0.17.1
|
||||
SQLAlchemy==1.4.15
|
||||
tqdm==4.60.0
|
||||
uWSGI==2.0.19.1
|
||||
Werkzeug==2.0.0
|
||||
zope.event==4.5.0
|
||||
zope.interface==5.4.0
|
||||
|
Loading…
Reference in New Issue
Block a user