Skip to content

Commit c526392

Browse files
committed
Initial commit of code
1 parent a54e954 commit c526392

6 files changed

Lines changed: 371 additions & 1 deletion

File tree

README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,80 @@
11
# GunStreamer
2-
Streaming component for Gun db.
2+
Streaming component for Gun db. This is only the streaming part. The viewer part is a different component. For now it will be published to the root of Gun. To verify that it is publishing you can view currently view it at: https://gunmeeting.herokuapp.com/
3+
4+
# Integration
5+
For an example use the index.html and the .js folder.
6+
7+
### HTML
8+
```html
9+
<head>
10+
...
11+
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
12+
<script type="text/javascript" src="js/GunRecorder.js"></script>
13+
<script type="text/javascript" src="js/GunStreamer.js"></script>
14+
...
15+
</head>
16+
```
17+
18+
```html
19+
<body>
20+
...
21+
<button type="button" onclick="gunRecorder.startCamera()">Start Camera</button>
22+
<button id="record_button" type="button" onclick="gunRecorder.record()">Start Recording</button>
23+
<br><br>
24+
<video id="record_video" width="20%" poster="https://www.srsd.net/images/video-poster.png" autoplay controls muted />
25+
<script type="text/javascript" src="js/initialiation.js"></script>
26+
...
27+
</body>
28+
```
29+
30+
### initialiation.js
31+
The gun part, writing to gun and publish it.
32+
```javascript
33+
//Configure GUN to pass to streamer
34+
var peers = ['https://gunmeetingserver.herokuapp.com/gun'];
35+
var opt = { peers: peers, localStorage: false, radisk: false };
36+
var gunDB = Gun(opt);
37+
38+
//Config for the GUN GunStreamer
39+
var streamer_config = {
40+
dbRecord: "gunmeeting",//The root of the streams
41+
streamId: "qvdev",//The user id you wanna stream
42+
gun: gunDB,//Gun instance
43+
debug: false//For debug logs
44+
}
45+
```
46+
The recorder part record parse and notify ondataavailable
47+
```javascript
48+
//GUN Streamer is the data side. It will convert data and write to GUN db
49+
const gunStreamer = new GunStreamer(streamer_config)
50+
51+
//This is a callback function about the recording state, following states possible
52+
// STOPPED: 1¸
53+
// RECORDING:2
54+
// NOT_AVAILABLE:3
55+
// UNKNOWN:4
56+
var onRecordStateChange = function (state) {
57+
var recordButton = document.getElementById("record_button");
58+
switch (state) {
59+
case recordSate.RECORDING:
60+
recordButton.innerText = "Stop recording";
61+
break;
62+
default:
63+
recordButton.innerText = "Start recording";
64+
break;
65+
}
66+
}
67+
68+
//Config for the gun recorder
69+
var recorder_config = {
70+
video_id: "record_video",//Video html element id
71+
onDataAvailable: gunStreamer.onDataAvailable,//MediaRecorder data available callback
72+
onRecordStateChange: onRecordStateChange,//Callback for recording state
73+
audioBitsPerSecond: 6000,//Audio bits per second this is the lowest quality
74+
videoBitsPerSecond: 100000,//Video bits per second this is the lowest quality
75+
debug: false//For debug logs
76+
}
77+
78+
//Init the recorder
79+
const gunRecorder = new GunRecorder(recorder_config);
80+
```

index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title></title>
7+
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
8+
<script type="text/javascript" src="js/GunRecorder.js"></script>
9+
<script type="text/javascript" src="js/GunStreamer.js"></script>
10+
</head>
11+
12+
<body>
13+
<button type="button" onclick="gunRecorder.startCamera()">Start Camera</button>
14+
<button id="record_button" type="button" onclick="gunRecorder.record()">Start Recording</button>
15+
<br><br>
16+
<video id="record_video" width="20%" poster="https://www.srsd.net/images/video-poster.png" autoplay controls muted />
17+
<script type="text/javascript" src="js/integration.js"></script>
18+
</body>
19+
20+
</html>

js/GunRecorder.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
var recordSate = {
2+
STOPPED: 1,
3+
RECORDING: 2,
4+
NOT_AVAILABLE: 3,
5+
UNKNOWN: 4,
6+
};
7+
8+
const MIMETYPE = 'video/webm; codecs="opus,vp8"';
9+
const RECORDER_TIME_SLICE = 300;
10+
const CAMERA_OPTIONS = { video: true, audio: true }
11+
12+
class GunRecorder {
13+
constructor(config) {
14+
this.video = document.getElementById(config.video_id);
15+
this.mediaRecorder = null;
16+
this.onDataAvailable = config.onDataAvailable;
17+
this.onRecordStateChange = config.onRecordStateChange
18+
this.recorderOptions = {
19+
mimeType: MIMETYPE,
20+
audioBitsPerSecond: config.audioBitsPerSecond,
21+
videoBitsPerSecond: config.videoBitsPerSecond
22+
}
23+
this.debug = config.debug;
24+
this.setRecordingState(recordSate.UNKNOWN);
25+
}
26+
27+
record() {
28+
if (this.recordSate == recordSate.RECORDING) {
29+
this.mediaRecorder.stop();
30+
this.changeRecordState();
31+
} else if (this.recordSate == recordSate.STOPPED) {
32+
this.mediaRecorder = new MediaRecorder(gunRecorder.video.captureStream(), this.recorderOptions);
33+
this.mediaRecorder.ondataavailable = gunRecorder.onDataAvailable;
34+
this.mediaRecorder.start(RECORDER_TIME_SLICE);
35+
this.changeRecordState();
36+
} else {
37+
this.debugLog("The camera has not been initialized yet. First call startCamera()")
38+
}
39+
}
40+
41+
startCamera() {
42+
if (this.recordSate == recordSate.RECORDING || this.recordSate == recordSate.STOPPED) {
43+
this.debugLog("Camera already started no need to do again");
44+
return;
45+
}
46+
var gunRecorder = this;
47+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
48+
navigator.mediaDevices.getUserMedia(CAMERA_OPTIONS).then(function (stream) {
49+
gunRecorder.video.srcObject = stream;
50+
gunRecorder.video.play();
51+
});
52+
this.setRecordingState(recordSate.STOPPED);
53+
} else {
54+
this.setRecordingState(recordSate.NOT_AVAILABLE);
55+
}
56+
}
57+
58+
changeRecordState() {
59+
switch (this.recordSate) {
60+
case recordSate.STOPPED:
61+
this.setRecordingState(recordSate.RECORDING);
62+
break;
63+
case recordSate.NOT_AVAILABLE:
64+
this.debugLog("Sorry camera not available")
65+
break;
66+
case recordSate.UNKNOWN:
67+
this.debugLog("State is unknown check if camera is intialized")
68+
break;
69+
default:
70+
this.setRecordingState(recordSate.STOPPED);
71+
break;
72+
}
73+
}
74+
75+
setRecordingState(recordSate) {
76+
this.debugLog("STATE BEFORE::" + this.recordSate);
77+
this.recordSate = recordSate;
78+
this.onRecordStateChange(this.recordSate);
79+
this.debugLog("STATE AFTER::" + this.recordSate);
80+
}
81+
82+
debugLog(logData) {
83+
if (this.debug) {
84+
console.log(logData);
85+
}
86+
}
87+
}

js/GunStreamer.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
const RECORD_PREFIX = "GkXf"
3+
var parseWorker
4+
var initialData
5+
6+
class GunStreamer {
7+
constructor(config) {
8+
this.dbRecord = config.dbRecord;
9+
this.streamId = config.streamId;
10+
this.gunDB = config.gun;
11+
this.debug = config.debug;
12+
this.startWorker();
13+
}
14+
15+
onDataAvailable(event) {
16+
if (event.data.size > 0) {
17+
var blob = event.data;
18+
var response = new Response(blob).arrayBuffer().then(function (arrayBuffer) {
19+
blob = null;
20+
if (parseWorker != undefined) {
21+
parseWorker.postMessage(arrayBuffer);
22+
}
23+
});
24+
response = null;
25+
} else {
26+
this.debugLog("data not available")
27+
}
28+
}
29+
30+
startWorker() {
31+
if (typeof (Worker) !== "undefined") {
32+
if (typeof (parseWorker) == "undefined") {
33+
parseWorker = new Worker("js/parser_worker.js");
34+
}
35+
parseWorker.onmessage = e => {
36+
const message = e.data;
37+
this.writeToGun(message);
38+
};
39+
} else {
40+
LOG("Sorry! No Web Worker support.");
41+
}
42+
}
43+
44+
stopWorker() {
45+
parseWorker.terminate();
46+
parseWorker = undefined;
47+
}
48+
49+
writeToGun(base64data) {
50+
this.debugLog("Write to GUN::" + base64data.substring(0, 100));
51+
let lastUpdate = new Date().getTime();
52+
let user;
53+
if (initialData == undefined && base64data.startsWith(RECORD_PREFIX)) {
54+
this.debugLog("INITIAL");
55+
var n = base64data.indexOf("wIEB");
56+
this.debugLog("RAW::" + n + "::" + base64data.substring(0, 252));
57+
initialData = base64data.substring(0, 252);
58+
} else {
59+
var n = base64data.indexOf("H0O2dQH");
60+
this.debugLog("RAW::" + n + "::" + base64data);
61+
}
62+
63+
//Probably has to be changed to different data structure
64+
user = gunDB.get(this.streamId).put({ initial: initialData, name: base64data, id: this.streamId, timestamp: lastUpdate, isSpeaking: false });
65+
gunDB.get(this.dbRecord).set(user);
66+
}
67+
68+
debugLog(logData) {
69+
if (this.debug) {
70+
console.log(logData);
71+
}
72+
}
73+
74+
}

js/integration.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//Configure GUN to pass to streamer
2+
var peers = ['https://gunmeetingserver.herokuapp.com/gun'];
3+
var opt = { peers: peers, localStorage: false, radisk: false };
4+
var gunDB = Gun(opt);
5+
6+
//Config for the GUN GunStreamer
7+
var streamer_config = {
8+
dbRecord: "gunmeeting",//The root of the streams
9+
streamId: "qvdev",//The user id you wanna stream
10+
gun: gunDB,//Gun instance
11+
debug: false//For debug logs
12+
}
13+
14+
//GUN Streamer is the data side. It will convert data and write to GUN db
15+
const gunStreamer = new GunStreamer(streamer_config)
16+
17+
//This is a callback function about the recording state, following states possible
18+
// STOPPED: 1¸
19+
// RECORDING:2
20+
// NOT_AVAILABLE:3
21+
// UNKNOWN:4
22+
var onRecordStateChange = function (state) {
23+
var recordButton = document.getElementById("record_button");
24+
switch (state) {
25+
case recordSate.RECORDING:
26+
recordButton.innerText = "Stop recording";
27+
break;
28+
default:
29+
recordButton.innerText = "Start recording";
30+
break;
31+
}
32+
}
33+
34+
//Config for the gun recorder
35+
var recorder_config = {
36+
video_id: "record_video",//Video html element id
37+
onDataAvailable: gunStreamer.onDataAvailable,//MediaRecorder data available callback
38+
onRecordStateChange: onRecordStateChange,//Callback for recording state
39+
audioBitsPerSecond: 6000,//Audio bits per second this is the lowest quality
40+
videoBitsPerSecond: 100000,//Video bits per second this is the lowest quality
41+
debug: false//For debug logs
42+
}
43+
44+
//Init the recorder
45+
const gunRecorder = new GunRecorder(recorder_config);

js/parser_worker.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
onmessage = e => {
2+
const message = e.data;
3+
parseSelf(message);
4+
};
5+
6+
var allClusterHex = "";
7+
var initSegment = "";
8+
9+
function hex2buf(hex) {
10+
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
11+
return parseInt(h, 16)
12+
})).buffer
13+
}
14+
15+
function buf2hex(buffer) { // buffer is an ArrayBuffer
16+
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
17+
}
18+
19+
function parseSelf(arrayBuffer) {
20+
21+
var hex = buf2hex(arrayBuffer);
22+
var initSeg;
23+
24+
var ebmlIndex = hex.indexOf("1a45dfa3");
25+
var clusterIndex = hex.indexOf("1f43b675");
26+
var trackIndex = hex.indexOf("1654ae6b");
27+
var cuesIndex = hex.indexOf("1c53bb6b");
28+
var segmentIndex = hex.indexOf("18538067");
29+
var infoIndex = hex.indexOf("1549a966");
30+
var seekIndex = hex.indexOf("114d9b74");
31+
32+
33+
if (ebmlIndex == -1 && clusterIndex == -1 && trackIndex == -1 && cuesIndex == -1 && segmentIndex == -1 && infoIndex == -1 && seekIndex == -1) {
34+
allClusterHex += hex;
35+
36+
}
37+
38+
39+
if (ebmlIndex != -1) {
40+
initSeg = hex.substring(ebmlIndex, clusterIndex);
41+
var initArray = new Uint8Array(hex2buf(initSeg));
42+
var base64String = btoa(
43+
new Uint8Array(initArray)
44+
.reduce((onData, byte) => onData + String.fromCharCode(byte), '')
45+
);
46+
postMessage(base64String);
47+
}
48+
49+
if (clusterIndex != -1) {
50+
if (allClusterHex.length != 0) {
51+
52+
allClusterHex += hex.substring(0, clusterIndex);
53+
54+
var clusters = new Uint8Array(hex2buf(allClusterHex));
55+
56+
var base64String = btoa(clusters.reduce((onData, byte) => onData + String.fromCharCode(byte), ''))
57+
58+
allClusterHex = "";
59+
postMessage(base64String);
60+
}
61+
var cluster = hex.substring(clusterIndex, hex.length);
62+
var dataIndex = cluster.indexOf("a3");
63+
var clusterStartString = cluster.substring(0, dataIndex + 2);
64+
allClusterHex += cluster;
65+
}
66+
}

0 commit comments

Comments
 (0)