commit fdf2cffd0238cd804077232fddecbb6a9f88acfb Author: Skylar Sadlier Date: Thu Mar 7 12:39:45 2024 -0700 initial commit working version diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..210cb12 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Don't handle line endings automatically +* -text \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..eac633f --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a53011 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,85 @@ +FROM ubuntu:focal + +ENV HOME /root +ENV DEBIAN_FRONTEND noninteractive +ENV LC_ALL C.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +RUN dpkg --add-architecture i386 && \ + apt-get update && apt-get -y install python2 python-is-python2 xvfb x11vnc xdotool wget tar supervisor net-tools fluxbox gnupg2 && \ + wget -O - https://dl.winehq.org/wine-builds/winehq.key | apt-key add - && \ + echo 'deb https://dl.winehq.org/wine-builds/ubuntu/ focal main' |tee /etc/apt/sources.list.d/winehq.list && \ + apt-get update && apt-get -y install \ + wmctrl \ + winehq-stable \ + libgl1:i386 bzip2 \ + gstreamer1.0-plugins-good \ + gstreamer1.0-pulseaudio \ + gstreamer1.0-tools \ + libglu1-mesa \ + libgtk2.0-0 \ + libncursesw5 \ + libopenal1 \ + libsdl-image1.2 \ + libsdl-ttf2.0-0 \ + libsdl1.2debian \ + libsndfile1 \ + pulseaudio \ + ucspi-tcp \ + cpulimit && \ + mkdir /opt/wine-stable/share/wine/mono && wget -O - https://dl.winehq.org/wine/wine-mono/9.0.0/wine-mono-9.0.0-x86.tar.xz | tar -xJv -C /opt/wine-stable/share/wine/mono && \ + apt-get -y full-upgrade && apt-get clean && rm -rf /var/lib/apt/lists/* + +# the following can be used to skip download and use local file +# COPY AgeOfTime-29.exe /root/AgeOfTime-29.exe + +ENV WINEPREFIX /root/prefix32 +ENV WINEARCH win64 +ENV DISPLAY :0 +ENV AOTDIR "$WINEPREFIX/drive_c/Program Files/AgeOfTime" + +RUN wget -P /mono https://dl.winehq.org/wine/wine-mono/9.0.0/wine-mono-9.0.0-x86.msi && \ + wineboot -f -u && sleep 10 && xvfb-run msiexec /i /mono/wine-mono-9.0.0-x86.msi /quiet +# wineboot -u && msiexec /i /opt/wine-stable/share/wine/gecko/wine-gecko-2.47.1-x86.msi && \ +# rm -rf /mono/wine-mono-4.9.4.msi + +# download and install AgeOfTime +RUN cd /root && \ +# nohup /usr/bin/Xvfb :0 -screen 0 1024x768x24 && sleep 5 && \ + wget https://ageoftime.com/files/AgeOfTime-29.exe && \ +# mkdir -p "$WINEPREFIX/drive_c/Program Files/" && \ + cd "$WINEPREFIX/drive_c/" && \ + chmod +x /root/AgeOfTime-29.exe && \ + xvfb-run wine /root/AgeOfTime-29.exe -s && \ + rm -f /root/AgeOfTime-29.exe +# mv "$WINEPREFIX/drive_c/Program Files/AgeOfTime" "$WINEPREFIX/drive_c/AgeOfTime" + +WORKDIR /root/ +RUN wget -O - https://github.com/novnc/noVNC/archive/v1.1.0.tar.gz | tar -xzv -C /root/ && mv /root/noVNC-1.1.0 /root/novnc && ln -s /root/novnc/vnc_lite.html /root/novnc/index.html && \ + wget -O - https://github.com/novnc/websockify/archive/v0.9.0.tar.gz | tar -xzv -C /root/ && mv /root/websockify-0.9.0 /root/novnc/utils/websockify + +# lets copy age of time files instead of install +# COPY ["AgeOfTime", "$WINEPREFIX/drive_c/Program Files/AgeOfTime"] + +# Force vnc_lite.html to be used for novnc, to avoid having the directory listing page. +# Additionally, turn off the control bar. Finally, add a hook to start audio. +COPY webaudio.js /root/novnc/core/ +RUN rm -f /root/novnc/index.html && ln -s /root/novnc/vnc_lite.html /root/novnc/index.html \ + && sed -i 's/display:flex/display:none/' /root/novnc/app/styles/base.css \ + && sed -i "/import RFB/a \ + import WebAudio from './core/webaudio.js'" \ + /root/novnc/vnc_lite.html \ + && sed -i "/function connected(e)/a \ + var wa = new WebAudio('ws://localhost:8081/websockify'); \ + document.getElementsByTagName('canvas')[0].addEventListener('keydown', e => { wa.start(); });" \ + /root/novnc/vnc_lite.html + +ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf +ADD restart-aot-crash.sh /root/restart-aot-crash.sh +ADD start-aot.sh /root/start-aot.sh +RUN chmod +x /root/restart-aot-crash.sh && chmod +x /root/start-aot.sh +EXPOSE 8080 + +USER root +CMD ["/usr/bin/supervisord"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f71055 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f5ee199 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +build: + docker build -t aot-wine-x11-novnc-docker . + +run: build + docker run --rm -p 18080:8080 aot-wine-x11-novnc-docker + +shell: build + docker run --rm -ti -p 18080:8080 aot-wine-x11-novnc-docker bash diff --git a/README.md b/README.md new file mode 100644 index 0000000..d680e1c --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +## aot-wine-x11-novnc-docker + +![Docker Image Size (tag)](https://img.shields.io/docker/image-size/skylord123/aot-wine-x11-novnc-docker/latest) +![Docker Pulls](https://img.shields.io/docker/pulls/skylord123/aot-wine-x11-novnc-docker) + +This docker image will run AgeOfTime game client under wine in an Ubuntu container with a novnc server for you to view the application. Uses supervisor to automatically restart failed processes. + +Runs a small script in the background that will automatically kill the process if a wine "Program Error" dialog is displayed (since it starts winedbg instead of just quitting). +This script also runs cpulimit against the PID of the AgeOfTime.exe process to limit how much CPU it can consume (only if `AOT_CPU_LIMIT` env variable is defined and not zero). + +This container runs: + +* Xvfb - X11 in a virtual framebuffer +* x11vnc - A VNC server that scrapes the above X11 server +* [noNVC](https://kanaka.github.io/noVNC/) - A HTML5 canvas vnc viewer +* pulseaudio - Audio server (AOT crashes without proper audio) +* audiostream & websockify_audio - these are supposed to pass audio to the VNC web session but currently doesn't work +* Fluxbox - a small window manager +* AgeOfTime.exe - The game executable + +This is a [trusted build](https://registry.hub.docker.com/u/skylord123/aot-wine-x11-novnc-docker/) +on the Docker Hub. + +## Run It + +Modify your docker-compose.yml file then: + + docker compose up + +Go to `http://localhost:8080` in your browser and you should see AgeOfTime boot up. + +## Issues + +* Audio isn't working correctly. Keeps crashing when supservisor tries to start it and shortly gives up. +* Console output of game isn't being sent to the contrainer stdout (would be nice to fix this so the container logs show the game logs) \ No newline at end of file diff --git a/client.conf b/client.conf new file mode 100644 index 0000000..439169f --- /dev/null +++ b/client.conf @@ -0,0 +1 @@ +default-server=unix:/tmp/pulseaudio.socket diff --git a/default.pa b/default.pa new file mode 100644 index 0000000..02942e5 --- /dev/null +++ b/default.pa @@ -0,0 +1,3 @@ +#!/usr/bin/pulseaudio -nF +load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1 +load-module module-always-sink diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..185cbea --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,19 @@ +services: + aotbot: + build: . + ports: + - "8080:8080" + environment: + # + AOT_CPU_LIMIT: 0 + # uncomment if you want to volume mount AgeOfTime +# volumes: +# - '/path/on/host/AgeOfTime:/root/prefix32/drive_c/Program Files (x86)/AgeOfTime' + deploy: +# resources: +# limits: +# cpus: '0.50' +# memory: 512M +# reservations: +# cpus: '0.25' +# memory: 512M \ No newline at end of file diff --git a/restart-aot-crash.sh b/restart-aot-crash.sh new file mode 100644 index 0000000..2f8cfe4 --- /dev/null +++ b/restart-aot-crash.sh @@ -0,0 +1,36 @@ +#!/bin/bash +export DISPLAY=":0.0" + +# this script checks for the presence of the wine "Program Error" +# dialog and if it is present it kills AgeOfTime and winedbg +# which causes supervisor to restart it + +# This script also applies cpu limits if the env variable AOT_CPU_LIMIT is set + +# Check if AOT_CPU_LIMIT is set and not zero +if [ -z "$AOT_CPU_LIMIT" ] || [ "$AOT_CPU_LIMIT" -eq 0 ]; then + SKIP_CPU_LIMIT=true +else + SKIP_CPU_LIMIT=false +fi + +while true; do + if wmctrl -l|awk '{$3=""; $2=""; $1=""; print $0}' | grep '^\s*Program Error$'; then +# echo "AOT Program Error detected" + kill $(pidof AgeOfTime.exe) + kill $(pidof winedbg) + elif ! $SKIP_CPU_LIMIT && pidof AgeOfTime.exe >/dev/null; then + # AgeOfTime.exe is running + # Ensure cpulimit is installed and get the PID of AgeOfTime.exe + AOT_PID=$(pidof AgeOfTime.exe) + # Check if cpulimit is already running for AgeOfTime.exe to avoid stacking multiple limits + if ! pgrep -f "cpulimit.*$AOT_PID" > /dev/null; then + # Apply cpulimit to AgeOfTime.exe to limit it to 2 cores and 50% usage + # Note: cpulimit doesn't directly support core limitation, so we adjust the overall CPU percentage assuming 2 cores + # For more precise control, consider taskset or cgroups + cpulimit -p $AOT_PID -l $AOT_CPU_LIMIT + fi + fi + + sleep 2 +done \ No newline at end of file diff --git a/start-aot.sh b/start-aot.sh new file mode 100644 index 0000000..796dd10 --- /dev/null +++ b/start-aot.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +export DISPLAY=:0 +export WINEARCH=win64 +export HOME=/root +export LANG=en_US.UTF-8 +#export WINEDEBUG="+alsa,+pulse,+winealsa,+winepulse,+d3d,+ddraw,+opengl,+winediag", +cd "/root/prefix32/drive_c/Program Files (x86)/AgeOfTime" +#sleep 5 +# have to start with wineconsole as wine will cause a crash +# for some weird reason. wine works fine outside of supervisor though strangely +wineconsole AgeOfTime.exe \ No newline at end of file diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..f8dd8c2 --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,94 @@ +[supervisord] +nodaemon=true + +[program:X11] +command=/usr/bin/Xvfb :0 -screen 0 1024x768x24 +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +priority=1 + +[program:x11vnc] +command=/usr/bin/x11vnc -noxrecord +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +priority=1 + +[program:novnc] +command=/root/novnc/utils/launch.sh --vnc localhost:5900 --listen 8080 +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +priority=1 + +[program:pulseaudio] +command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1 +stdout_logfile=/root/pulseaudio.log +redirect_stderr=true +priority=1 + +[program:audiostream] +command=tcpserver localhost 5901 gst-launch-1.0 -q pulsesrc server=/tmp/pulseaudio.socket ! audio/x-raw, channels=2, rate=24000 ! cutter ! opusenc ! webmmux ! fdsink fd=1 +stdout_logfile=/root/audiostream.log +redirect_stderr=true +priority=1 + +[program:websockify_audio] +command=websockify 8081 localhost:5901 +stdout_logfile=/root/websockify-audio.log +redirect_stderr=true +priority=1 + +[program:explorer] +command=/opt/wine-stable/bin/wine Explorer.exe +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +priority=1 + +[program:fluxbox] +command=/usr/bin/fluxbox +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +priority=1 + +[program:restart-aot-if-crashed] +command=/bin/bash -c "/root/restart-aot-crash.sh" +umask=0022 +stderr_logfile = /var/log/supervisor/restart-aot-if-crashed-stderr.log +stdout_logfile = /var/log/supervisor/restart-aot-if-crashed-stdout.log +autostart=true +autorestart=true +user=root +redirect_stderr=true +environment=AOT_CPU_LIMIT=%(ENV_AOT_CPU_LIMIT)s + +[program:ageoftime] +command=/root/start-aot.sh +umask=0022 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true +autorestart=true +user=root +redirect_stderr=true +priority=200 +environment= + LANGUAGE="en_US.UTF-8", + WINEARCH="win32", + HOME="/root", + LANG="en_US.UTF-8", + WINEPREFIX="/root/prefix32", + TERM="xterm", + DISPLAY=":0", + LC_ALL="C.UTF-8", + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ No newline at end of file diff --git a/webaudio.js b/webaudio.js new file mode 100644 index 0000000..6d2d8f3 --- /dev/null +++ b/webaudio.js @@ -0,0 +1,121 @@ +export default class WebAudio { + constructor(url) { + this.url = url + + this.connected = false; + + //constants for audio behavoir + this.maximumAudioLag = 1.5; //amount of seconds we can potentially be behind the server audio stream + this.syncLagInterval = 5000; //check every x milliseconds if we are behind the server audio stream + this.updateBufferEvery = 20; //add recieved data to the player buffer every x milliseconds + this.reduceBufferInterval = 500; //trim the output audio stream buffer every x milliseconds so we don't overflow + this.maximumSecondsOfBuffering = 1; //maximum amount of data to store in the play buffer + this.connectionCheckInterval = 500; //check the connection every x milliseconds + + //register all our background timers. these need to be created only once - and will run independent of the object's streams/properties + setInterval(() => this.updateQueue(), this.updateBufferEvery); + setInterval(() => this.syncInterval(), this.syncLagInterval); + setInterval(() => this.reduceBuffer(), this.reduceBufferInterval); + setInterval(() => this.tryLastPacket(), this.connectionCheckInterval); + + } + + //registers all the event handlers for when this stream is closed - or when data arrives. + registerHandlers() { + this.mediaSource.addEventListener('sourceended', e => this.socketDisconnected(e)) + this.mediaSource.addEventListener('sourceclose', e => this.socketDisconnected(e)) + this.mediaSource.addEventListener('error', e => this.socketDisconnected(e)) + this.buffer.addEventListener('error', e => this.socketDisconnected(e)) + this.buffer.addEventListener('abort', e => this.socketDisconnected(e)) + } + + //starts the web audio stream. only call this method on button click. + start() { + if (!!this.connected) return; + if (!!this.audio) this.audio.remove(); + this.queue = null; + + this.mediaSource = new MediaSource() + this.mediaSource.addEventListener('sourceopen', e => this.onSourceOpen()) + //first we need a media source - and an audio object that contains it. + this.audio = document.createElement('audio'); + this.audio.src = window.URL.createObjectURL(this.mediaSource); + + //start our stream - we can only do this on user input + this.audio.play(); + } + + wsConnect() { + if (!!this.socket) this.socket.close(); + + this.socket = new WebSocket(this.url, ['binary', 'base64']) + this.socket.binaryType = 'arraybuffer' + this.socket.addEventListener('message', e => this.websocketDataArrived(e), false); + } + + //this is called when the media source contains data + onSourceOpen(e) { + this.buffer = this.mediaSource.addSourceBuffer('audio/webm; codecs="opus"') + this.registerHandlers(); + this.wsConnect(); + } + + //whenever data arrives in our websocket this is called. + websocketDataArrived(e) { + this.lastPacket = Date.now(); + this.connected = true; + this.queue = this.queue == null ? e.data : this.concat(this.queue, e.data); + } + + //whenever a disconnect happens this is called. + socketDisconnected(e) { + console.log(e); + this.connected = false; + } + + tryLastPacket() { + if (this.lastPacket == null) return; + if ((Date.now() - this.lastPacket) > 1000) { + this.socketDisconnected('timeout'); + } + } + + //this updates the buffer with the data from our queue + updateQueue() { + if (!(!!this.queue && !!this.buffer && !this.buffer.updating)) { + return; + } + + this.buffer.appendBuffer(this.queue); + this.queue = null; + } + + //reduces the stream buffer to the minimal size that we need for streaming + reduceBuffer() { + if (!(this.buffer && !this.buffer.updating && !!this.audio && !!this.audio.currentTime && this.audio.currentTime > 1)) { + return; + } + + this.buffer.remove(0, this.audio.currentTime - 1); + } + + //synchronizes the current time of the stream with the server + syncInterval() { + if (!(this.audio && this.audio.currentTime && this.audio.currentTime > 1 && this.buffer && this.buffer.buffered && this.buffer.buffered.length > 1)) { + return; + } + + var currentTime = this.audio.currentTime; + var targetTime = this.buffer.buffered.end(this.buffer.buffered.length - 1); + + if (targetTime > (currentTime + this.maximumAudioLag)) this.audio.fastSeek(targetTime); + } + + //joins two data arrays - helper function + concat(buffer1, buffer2) { + var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); + tmp.set(new Uint8Array(buffer1), 0); + tmp.set(new Uint8Array(buffer2), buffer1.byteLength); + return tmp.buffer; + }; +}