|
"use strict"; |
|
let app = new Vue({ |
|
el: '#app', |
|
data: () => ({ |
|
videoStream: null, |
|
audioStream: null, |
|
videoRecorder: null, |
|
recordOptions: { |
|
audioBitsPersecond: 128000, |
|
videoBitsPersecond: 250000, |
|
mimeType: 'video/webm;codecs=vp9' |
|
}, |
|
recordInterval: 100, |
|
chatTarget: "", |
|
showJoin: false, |
|
videoTarget: "", |
|
hideSocket: false, |
|
audioRecorder: null, |
|
chatMessage: "", |
|
chatUser: localStorage.getItem("chat-user"), |
|
wschat: null, |
|
chatSocket: localStorage.getItem("chat-socket") || "/ws/irc", |
|
chatChannel: window.location.hash || localStorage.getItem("chat-channel") || "#general", |
|
chatMessages: [], |
|
type: "getUserMedia", |
|
incomingStreams: [], |
|
incomingVideoSource: null, |
|
connectErrors: null, |
|
incomingVideoSourceBuffer: null, |
|
incomingVideoChunks: [], |
|
incomingVideoURL: null, |
|
errors: null |
|
}), |
|
watch: { |
|
videoStream(video, oldVideo) { |
|
if (oldVideo) this.stopTracks(oldVideo); |
|
if (video) { |
|
this.startTracks(video); |
|
this.videoRecorder = new MediaRecorder(video, this.recordOptions); |
|
this.videoRecorder.ondataavailable = this.onVideoRecorded; |
|
this.videoRecorder.start(parseInt(this.recordInterval)); |
|
} else { |
|
this.wschat.send(`PRIVMSG ${this.videoTarget} :\u0001STOPVIDEO\u0001`); |
|
} |
|
this.$refs['video-out'].srcObject = video; |
|
}, |
|
audioStream(value, oldValue) { |
|
if (oldValue) this.stopTracks(oldValue); |
|
if (value) this.startTracks(value); |
|
}, |
|
chatChannel(value) { localStorage.setItem("chat-channel", value); }, |
|
chatUser(value) { localStorage.setItem("chat-user", value); }, |
|
chatSocket(value) { localStorage.setItem("chat-socket", value); }, |
|
"chatMessages.length": function (value) { |
|
this.$nextTick(() => { |
|
if (!this.$refs.chatlog) return; |
|
this.$refs.chatlog.lastChild.scrollIntoView(); |
|
// console.log("chatlog", value, this.$refs.chatlog); |
|
}); |
|
}, |
|
"wschat.joinedChannels": function (value) { |
|
if (value.length) { |
|
if (!this.chatTarget) this.chatTarget = value[0].name; |
|
if (!this.videoTarget) this.videoTarget = value[0].name; |
|
} |
|
} |
|
}, |
|
methods: { |
|
stopTracks(stream) { |
|
stream.getTracks().forEach(track => track.stop()); |
|
}, |
|
startTracks(stream) { |
|
stream.getTracks().forEach(track => track.addEventListener('ended', () => this.stopVideo())); |
|
}, |
|
logError(err) { |
|
let e = { error: err, name: err.name, message: err.message }; |
|
if (this.errors) this.errors.push(e); |
|
else this.errors = [e]; |
|
}, |
|
startVideo() { |
|
navigator.mediaDevices[this.type]({ video: true }).then(stream => { |
|
this.videoStream = stream; |
|
}).catch(this.logError); |
|
}, |
|
stopVideo() { |
|
this.videoStream = null; |
|
}, |
|
startAudio() { |
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { |
|
this.audioStream = stream; |
|
}).catch(this.logError); |
|
}, |
|
stopAudio() { |
|
this.audioStream = null; |
|
}, |
|
connectChat() { |
|
this.wschat = new WSChat(this.chatSocket, { |
|
onconnect: () => { |
|
this.$forceUpdate(); |
|
this.wschat.login(this.chatUser); |
|
this.wschat.onCommand("PRIVMSG", this.onChatMessage); |
|
}, |
|
onclose: () => { |
|
this.stopVideo(); |
|
this.stopAudio(); |
|
this.$forceUpdate(); |
|
}, |
|
onmessage: (msg) => { if (msg.command !== "PRIVMSG") console.log("Received message: ", msg) }, |
|
onerror: (err) => { |
|
if (!this.connectErrors) this.connectErrors = []; |
|
this.connectErrors.push(err); |
|
} |
|
}); |
|
}, |
|
onVideoRecorded(ev) { |
|
let reader = new FileReader(); |
|
let chunkSize = 400; |
|
reader.onload = () => { |
|
let result = reader.result; |
|
let n = Math.ceil(result.length / chunkSize); |
|
for (let i = 0; i < n; i++) { |
|
let chunk = result.slice(i * chunkSize, (i + 1) * chunkSize); |
|
this.wschat.send(`PRIVMSG ${this.videoTarget} :\u0001VIDEO ${ev.timecode} ${i + 1} ${n} ${chunk}\u0001`); |
|
} |
|
} |
|
reader.readAsDataURL(ev.data); |
|
}, |
|
sendChatMessage() { |
|
let msg = this.chatMessage; |
|
if (!msg) return; |
|
this.chatMessages.push({ source: "(me)", parameters: `${this.chatTarget} :${msg}` }); |
|
this.chatMessage = ""; |
|
this.wschat.send(`PRIVMSG ${this.chatTarget} :${msg}`); |
|
}, |
|
onChatMessage(msg) { |
|
if (msg.parameters.slice(msg.msgIdx, msg.msgIdx + 8) === ":\u0001VIDEO ") { |
|
let endMark = msg.parameters.indexOf("\u0001", msg.msgIdx + 2); |
|
if (endMark > 0) |
|
this.onVideoChunk(msg.sourceNick || msg.source, msg.target, msg.parameters.slice(msg.msgIdx + 8, endMark)); |
|
else |
|
console.warn("Chunk not fully received: ", msg); |
|
} else if (msg.parameters.slice(msg.msgIdx, msg.msgIdx + 12) === ":\u0001STOPVIDEO\u0001") { |
|
let idx = this.incomingStreams.findIndex(e => e.source === msg.sourceNick); |
|
if (idx >= 0) this.incomingStreams.splice(idx, 1); |
|
} else |
|
this.chatMessages.push(msg); |
|
}, |
|
onVideoChunk(source, target, chunk) { |
|
let spc1 = chunk.indexOf(" "); |
|
let spc2 = chunk.indexOf(" ", spc1 + 1); |
|
let spc3 = chunk.indexOf(" ", spc2 + 1); |
|
let c = { |
|
id: chunk.slice(0, spc1), |
|
i: parseInt(chunk.slice(spc1 + 1, spc2)) - 1, |
|
n: parseInt(chunk.slice(spc2 + 1, spc3)), |
|
} |
|
let sourceStream = this.incomingStreams.filter(s => s.source === source)[0]; |
|
if (!sourceStream) { |
|
sourceStream = { |
|
source: source, target: target, mediaSource: new MediaSource(), incomingChunks: {}, |
|
videoType: chunk.slice(spc3 + 6, chunk.indexOf(";base64,", spc3)), |
|
frames: [], canReceiveVideo: true, |
|
}; |
|
sourceStream.mediaSource.onsourceopen = () => { |
|
console.log("mediaSource onsourceopen, available frames:", sourceStream.frames.length); |
|
sourceStream.ready = true; |
|
sourceStream.sourceBuffer = sourceStream.mediaSource.addSourceBuffer(sourceStream.videoType); |
|
sourceStream.sourceBuffer.onupdateend = () => { |
|
if (sourceStream.frames.length > 0) |
|
sourceStream.sourceBuffer.appendBuffer(sourceStream.frames.shift()); |
|
else |
|
sourceStream.canReceiveVideo = true; |
|
}; |
|
} |
|
sourceStream.mediaSource.onsourceclose = () => { |
|
console.log("mediaSource onsourceclose, video && video error: ", sourceStream.dom, sourceStream.dom && sourceStream.dom.error); |
|
sourceStream.ready = false; |
|
} |
|
sourceStream.url = URL.createObjectURL(sourceStream.mediaSource); |
|
this.incomingStreams.push(sourceStream); |
|
} |
|
if (!(c.id in sourceStream.incomingChunks)) |
|
sourceStream.incomingChunks[c.id] = new Array(c.n); |
|
let chunkArray = sourceStream.incomingChunks[c.id]; |
|
chunkArray[c.i] = chunk.slice(spc3 + 1); |
|
if (chunkArray.filter(e => typeof (e) === "string").length === chunkArray.length) { |
|
let b64data = chunkArray.join("").slice(chunkArray[0].indexOf(",") + 1); |
|
let binary; |
|
try { binary = atob(b64data); } |
|
catch (e) { |
|
console.log("Could not decode: ", b64data); |
|
return; |
|
} |
|
let buffer = new ArrayBuffer(binary.length); |
|
let intBuffer = new Uint8Array(buffer); |
|
for (let i = 0; i < binary.length; i++) intBuffer[i] = binary.charCodeAt(i); |
|
sourceStream.frames.push(buffer); |
|
delete sourceStream.incomingChunks[c.id]; |
|
} |
|
let v; |
|
if (!sourceStream.dom && (v = (this.$refs[`video-in-${sourceStream.source}`]))) |
|
sourceStream.dom = v.shift(); |
|
while (sourceStream.frames.length > 0 && sourceStream.canReceiveVideo && sourceStream.sourceBuffer) { |
|
if (sourceStream.dom && sourceStream.dom.error) |
|
console.log("video.error:", sourceStream.dom.error); |
|
else |
|
sourceStream.sourceBuffer.appendBuffer(sourceStream.frames.shift()); |
|
sourceStream.canReceiveVideo = false; |
|
} |
|
}, |
|
onVideoReceived(ev) { |
|
if (!this.incomingVideoSource) { |
|
this.incomingVideoSource = new MediaSource(); |
|
this.incomingVideoURL = URL.createObjectURL(this.incomingVideoSource); |
|
this.incomingVideoSource.onsourceopen = () => { |
|
this.incomingVideoSourceBuffer = this.incomingVideoSource.addSourceBuffer(ev.data.type); |
|
this.incomingVideoChunks.push(ev.data); |
|
} |
|
} else if (!this.incomingVideoSourceBuffer) { |
|
this.incomingVideoChunks.push(ev.data); |
|
} else { |
|
this.incomingVideoChunks.push(ev.data); |
|
this.incomingVideoChunks.splice(0, 1)[0].arrayBuffer().then(a => this.incomingVideoSourceBuffer.appendBuffer(a)); |
|
} |
|
}, |
|
recordAudio: console.log.bind(null, "TBD: recordAudio"), |
|
} |
|
}); |