voice start
This commit is contained in:
parent
68fcc7a135
commit
369f30e5fc
6 changed files with 622 additions and 98 deletions
|
@ -1,6 +1,6 @@
|
||||||
import{ getBulkInfo }from"./login.js";
|
import{ getBulkInfo }from"./login.js";
|
||||||
|
|
||||||
class Voice{
|
class AVoice{
|
||||||
audioCtx: AudioContext;
|
audioCtx: AudioContext;
|
||||||
info: { wave: string | Function; freq: number };
|
info: { wave: string | Function; freq: number };
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
|
@ -95,7 +95,7 @@ class Voice{
|
||||||
static noises(noise: string): void{
|
static noises(noise: string): void{
|
||||||
switch(noise){
|
switch(noise){
|
||||||
case"three": {
|
case"three": {
|
||||||
const voicy = new Voice("sin", 800);
|
const voicy = new AVoice("sin", 800);
|
||||||
voicy.play();
|
voicy.play();
|
||||||
setTimeout(_=>{
|
setTimeout(_=>{
|
||||||
voicy.freq = 1000;
|
voicy.freq = 1000;
|
||||||
|
@ -109,7 +109,7 @@ class Voice{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case"zip": {
|
case"zip": {
|
||||||
const voicy = new Voice((t: number, freq: number)=>{
|
const voicy = new AVoice((t: number, freq: number)=>{
|
||||||
return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq);
|
return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq);
|
||||||
}, 700);
|
}, 700);
|
||||||
voicy.play();
|
voicy.play();
|
||||||
|
@ -119,7 +119,7 @@ class Voice{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case"square": {
|
case"square": {
|
||||||
const voicy = new Voice("square", 600, 0.4);
|
const voicy = new AVoice("square", 600, 0.4);
|
||||||
voicy.play();
|
voicy.play();
|
||||||
setTimeout(_=>{
|
setTimeout(_=>{
|
||||||
voicy.freq = 800;
|
voicy.freq = 800;
|
||||||
|
@ -133,7 +133,7 @@ class Voice{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case"beep": {
|
case"beep": {
|
||||||
const voicy = new Voice("sin", 800);
|
const voicy = new AVoice("sin", 800);
|
||||||
voicy.play();
|
voicy.play();
|
||||||
setTimeout(_=>{
|
setTimeout(_=>{
|
||||||
voicy.stop();
|
voicy.stop();
|
||||||
|
@ -161,4 +161,4 @@ class Voice{
|
||||||
return userinfos.preferences.notisound;
|
return userinfos.preferences.notisound;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export{ Voice };
|
export{ AVoice as AVoice };
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
import{ Message }from"./message.js";
|
import{ Message }from"./message.js";
|
||||||
import{ Voice }from"./audio.js";
|
import{ AVoice }from"./audio.js";
|
||||||
import{ Contextmenu }from"./contextmenu.js";
|
import{ Contextmenu }from"./contextmenu.js";
|
||||||
import{ Dialog }from"./dialog.js";
|
import{ Dialog }from"./dialog.js";
|
||||||
import{ Guild }from"./guild.js";
|
import{ Guild }from"./guild.js";
|
||||||
|
@ -10,16 +10,10 @@ import{ Settings }from"./settings.js";
|
||||||
import{ Role, RoleList }from"./role.js";
|
import{ Role, RoleList }from"./role.js";
|
||||||
import{ InfiniteScroller }from"./infiniteScroller.js";
|
import{ InfiniteScroller }from"./infiniteScroller.js";
|
||||||
import{ SnowFlake }from"./snowflake.js";
|
import{ SnowFlake }from"./snowflake.js";
|
||||||
import{
|
import{channeljson,embedjson,messageCreateJson,messagejson,readyjson,startTypingjson}from"./jsontypes.js";
|
||||||
channeljson,
|
|
||||||
embedjson,
|
|
||||||
messageCreateJson,
|
|
||||||
messagejson,
|
|
||||||
readyjson,
|
|
||||||
startTypingjson,
|
|
||||||
}from"./jsontypes.js";
|
|
||||||
import{ MarkDown }from"./markdown.js";
|
import{ MarkDown }from"./markdown.js";
|
||||||
import{ Member }from"./member.js";
|
import{ Member }from"./member.js";
|
||||||
|
import { Voice } from "./voice.js";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface NotificationOptions {
|
interface NotificationOptions {
|
||||||
|
@ -55,6 +49,7 @@ class Channel extends SnowFlake{
|
||||||
idToPrev: Map<string, string> = new Map();
|
idToPrev: Map<string, string> = new Map();
|
||||||
idToNext: Map<string, string> = new Map();
|
idToNext: Map<string, string> = new Map();
|
||||||
messages: Map<string, Message> = new Map();
|
messages: Map<string, Message> = new Map();
|
||||||
|
voice?:Voice;
|
||||||
static setupcontextmenu(){
|
static setupcontextmenu(){
|
||||||
this.contextmenu.addbutton("Copy channel id", function(this: Channel){
|
this.contextmenu.addbutton("Copy channel id", function(this: Channel){
|
||||||
navigator.clipboard.writeText(this.id);
|
navigator.clipboard.writeText(this.id);
|
||||||
|
@ -336,6 +331,9 @@ class Channel extends SnowFlake{
|
||||||
}
|
}
|
||||||
this.setUpInfiniteScroller();
|
this.setUpInfiniteScroller();
|
||||||
this.perminfo ??= {};
|
this.perminfo ??= {};
|
||||||
|
if(this.type===2){
|
||||||
|
this.voice=new Voice(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
get perminfo(){
|
get perminfo(){
|
||||||
return this.guild.perminfo.channels[this.id];
|
return this.guild.perminfo.channels[this.id];
|
||||||
|
@ -840,6 +838,9 @@ class Channel extends SnowFlake{
|
||||||
loading.classList.add("loading");
|
loading.classList.add("loading");
|
||||||
this.rendertyping();
|
this.rendertyping();
|
||||||
this.localuser.getSidePannel();
|
this.localuser.getSidePannel();
|
||||||
|
if(this.voice){
|
||||||
|
this.voice.join();
|
||||||
|
}
|
||||||
await this.putmessages();
|
await this.putmessages();
|
||||||
await prom;
|
await prom;
|
||||||
if(id !== Channel.genid){
|
if(id !== Channel.genid){
|
||||||
|
@ -1334,7 +1335,7 @@ class Channel extends SnowFlake{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
notify(message: Message, deep = 0){
|
notify(message: Message, deep = 0){
|
||||||
Voice.noises(Voice.getNotificationSound());
|
AVoice.noises(AVoice.getNotificationSound());
|
||||||
if(!("Notification" in window)){
|
if(!("Notification" in window)){
|
||||||
}else if(Notification.permission === "granted"){
|
}else if(Notification.permission === "granted"){
|
||||||
let noticontent: string | undefined | null = message.content.textContent;
|
let noticontent: string | undefined | null = message.content.textContent;
|
||||||
|
|
|
@ -408,79 +408,109 @@ type wsjson =
|
||||||
| "MESSAGE_REACTION_REMOVE_EMOJI";
|
| "MESSAGE_REACTION_REMOVE_EMOJI";
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 0;
|
op: 0;
|
||||||
t: "GUILD_MEMBERS_CHUNK";
|
t: "GUILD_MEMBERS_CHUNK";
|
||||||
d: memberChunk;
|
d: memberChunk;
|
||||||
s: number;
|
s: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 0;
|
op: 0;
|
||||||
d: {
|
d: {
|
||||||
id: string;
|
id: string;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
};
|
};
|
||||||
s: number;
|
s: number;
|
||||||
t: "MESSAGE_DELETE";
|
t: "MESSAGE_DELETE";
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 0;
|
op: 0;
|
||||||
d: {
|
d: {
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
} & messagejson;
|
} & messagejson;
|
||||||
s: number;
|
s: number;
|
||||||
t: "MESSAGE_UPDATE";
|
t: "MESSAGE_UPDATE";
|
||||||
}
|
}
|
||||||
| messageCreateJson
|
| messageCreateJson
|
||||||
| readyjson
|
| readyjson
|
||||||
| {
|
| {
|
||||||
op: 11;
|
op: 11;
|
||||||
s: undefined;
|
s: undefined;
|
||||||
d: {};
|
d: {};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 10;
|
op: 10;
|
||||||
s: undefined;
|
s: undefined;
|
||||||
d: {
|
d: {
|
||||||
heartbeat_interval: number;
|
heartbeat_interval: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 0;
|
op: 0;
|
||||||
t: "MESSAGE_REACTION_ADD";
|
t: "MESSAGE_REACTION_ADD";
|
||||||
d: {
|
d: {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
message_id: string;
|
message_id: string;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
emoji: emojijson;
|
emoji: emojijson;
|
||||||
member?: memberjson;
|
member?: memberjson;
|
||||||
};
|
};
|
||||||
s: number;
|
s: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 0;
|
op: 0;
|
||||||
t: "MESSAGE_REACTION_REMOVE";
|
t: "MESSAGE_REACTION_REMOVE";
|
||||||
d: {
|
d: {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
message_id: string;
|
message_id: string;
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
emoji: emojijson;
|
emoji: emojijson;
|
||||||
};
|
};
|
||||||
s: 3;
|
s: 3;
|
||||||
}|memberlistupdatejson;
|
}|memberlistupdatejson|voiceupdate|voiceserverupdate;
|
||||||
type memberChunk = {
|
|
||||||
guild_id: string;
|
|
||||||
nonce: string;
|
|
||||||
members: memberjson[];
|
|
||||||
presences: presencejson[];
|
|
||||||
chunk_index: number;
|
|
||||||
chunk_count: number;
|
|
||||||
not_found: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
type memberChunk = {
|
||||||
|
guild_id: string;
|
||||||
|
nonce: string;
|
||||||
|
members: memberjson[];
|
||||||
|
presences: presencejson[];
|
||||||
|
chunk_index: number;
|
||||||
|
chunk_count: number;
|
||||||
|
not_found: string[];
|
||||||
|
};
|
||||||
|
type voiceupdate={
|
||||||
|
op: 0,
|
||||||
|
t: "VOICE_STATE_UPDATE",
|
||||||
|
d: {
|
||||||
|
guild_id: string,
|
||||||
|
channel_id: string,
|
||||||
|
user_id: string,
|
||||||
|
member: memberjson,
|
||||||
|
session_id: string,
|
||||||
|
token: string,
|
||||||
|
deaf: boolean,
|
||||||
|
mute: boolean,
|
||||||
|
self_deaf: boolean,
|
||||||
|
self_mute: boolean,
|
||||||
|
self_video: boolean,
|
||||||
|
suppress: boolean
|
||||||
|
},
|
||||||
|
s: number
|
||||||
|
};
|
||||||
|
type voiceserverupdate={
|
||||||
|
op: 0,
|
||||||
|
t: "VOICE_SERVER_UPDATE",
|
||||||
|
d: {
|
||||||
|
token: string,
|
||||||
|
guild_id: string,
|
||||||
|
endpoint: string
|
||||||
|
},
|
||||||
|
s: 6
|
||||||
|
};
|
||||||
type memberlistupdatejson={
|
type memberlistupdatejson={
|
||||||
op: 0,
|
op: 0,
|
||||||
s: number,
|
s: number,
|
||||||
|
@ -513,6 +543,65 @@ type memberlistupdatejson={
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
type webRTCSocket= {
|
||||||
|
op: 8,
|
||||||
|
d: {
|
||||||
|
heartbeat_interval: number
|
||||||
|
}
|
||||||
|
}|{
|
||||||
|
op:6,
|
||||||
|
d:{t: number}
|
||||||
|
}|{
|
||||||
|
op: 2,
|
||||||
|
d: {
|
||||||
|
ssrc: number,
|
||||||
|
"streams": {
|
||||||
|
type: "video",//probally more options, but idk
|
||||||
|
rid: string,
|
||||||
|
quality: number,
|
||||||
|
ssrc: number,
|
||||||
|
rtx_ssrc:number
|
||||||
|
}[],
|
||||||
|
ip: number,
|
||||||
|
port: number,
|
||||||
|
"modes": [],//no clue
|
||||||
|
"experiments": []//no clue
|
||||||
|
}
|
||||||
|
}|sdpback|opRTC12;
|
||||||
|
type sdpback={
|
||||||
|
op: 4,
|
||||||
|
d: {
|
||||||
|
audioCodec: string,
|
||||||
|
videoCodec: string,
|
||||||
|
media_session_id: string,
|
||||||
|
sdp: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
type opRTC12={
|
||||||
|
op: 12,
|
||||||
|
d: {
|
||||||
|
user_id: string,
|
||||||
|
audio_ssrc: number,
|
||||||
|
video_ssrc: number,
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
rid: "100",
|
||||||
|
ssrc: number,
|
||||||
|
active: boolean,
|
||||||
|
quality: 100,
|
||||||
|
rtx_ssrc: number,
|
||||||
|
max_bitrate: 2500000,
|
||||||
|
max_framerate: number,
|
||||||
|
max_resolution: {
|
||||||
|
type: "fixed",
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
export{
|
export{
|
||||||
readyjson,
|
readyjson,
|
||||||
dirrectjson,
|
dirrectjson,
|
||||||
|
@ -532,5 +621,10 @@ export{
|
||||||
messageCreateJson,
|
messageCreateJson,
|
||||||
memberChunk,
|
memberChunk,
|
||||||
invitejson,
|
invitejson,
|
||||||
memberlistupdatejson
|
memberlistupdatejson,
|
||||||
|
voiceupdate,
|
||||||
|
voiceserverupdate,
|
||||||
|
webRTCSocket,
|
||||||
|
sdpback,
|
||||||
|
opRTC12
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,27 +1,17 @@
|
||||||
import{ Guild }from"./guild.js";
|
import{ Guild }from"./guild.js";
|
||||||
import{ Channel }from"./channel.js";
|
import{ Channel }from"./channel.js";
|
||||||
import{ Direct }from"./direct.js";
|
import{ Direct }from"./direct.js";
|
||||||
import{ Voice }from"./audio.js";
|
import{ AVoice }from"./audio.js";
|
||||||
import{ User }from"./user.js";
|
import{ User }from"./user.js";
|
||||||
import{ Dialog }from"./dialog.js";
|
import{ Dialog }from"./dialog.js";
|
||||||
import{ getapiurls, getBulkInfo, setTheme, Specialuser }from"./login.js";
|
import{ getapiurls, getBulkInfo, setTheme, Specialuser }from"./login.js";
|
||||||
import{
|
import{channeljson,guildjson,mainuserjson,memberjson,memberlistupdatejson,messageCreateJson,presencejson,readyjson,startTypingjson,voiceupdate,wsjson,}from"./jsontypes.js";
|
||||||
channeljson,
|
|
||||||
guildjson,
|
|
||||||
mainuserjson,
|
|
||||||
memberjson,
|
|
||||||
memberlistupdatejson,
|
|
||||||
messageCreateJson,
|
|
||||||
presencejson,
|
|
||||||
readyjson,
|
|
||||||
startTypingjson,
|
|
||||||
wsjson,
|
|
||||||
}from"./jsontypes.js";
|
|
||||||
import{ Member }from"./member.js";
|
import{ Member }from"./member.js";
|
||||||
import{ Form, FormError, Options, Settings }from"./settings.js";
|
import{ Form, FormError, Options, Settings }from"./settings.js";
|
||||||
import{ MarkDown }from"./markdown.js";
|
import{ MarkDown }from"./markdown.js";
|
||||||
import { Bot } from "./bot.js";
|
import { Bot } from "./bot.js";
|
||||||
import { Role } from "./role.js";
|
import { Role } from "./role.js";
|
||||||
|
import { Voice } from "./voice.js";
|
||||||
|
|
||||||
const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]);
|
const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]);
|
||||||
|
|
||||||
|
@ -486,8 +476,21 @@ class Localuser{
|
||||||
this.memberListUpdate(temp)
|
this.memberListUpdate(temp)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VOICE_STATE_UPDATE":
|
||||||
|
if(this.waitingForVoice){
|
||||||
|
this.waitingForVoice(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "VOICE_SERVER_UPDATE":
|
||||||
|
if(this.currentVoice){
|
||||||
|
Voice.url=temp.d.endpoint;
|
||||||
|
Voice.gotUrl();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}else if(temp.op === 10){
|
}else if(temp.op === 10){
|
||||||
if(!this.ws)return;
|
if(!this.ws)return;
|
||||||
console.log("heartbeat down");
|
console.log("heartbeat down");
|
||||||
|
@ -501,6 +504,29 @@ class Localuser{
|
||||||
}, this.heartbeat_interval);
|
}, this.heartbeat_interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
waitingForVoice?:((arg:voiceupdate|undefined)=>void);
|
||||||
|
currentVoice?:Voice;
|
||||||
|
async joinVoice(voice:Voice){
|
||||||
|
this.currentVoice=voice;
|
||||||
|
if(this.ws){
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
d:{
|
||||||
|
guild_id: voice.guild.id,
|
||||||
|
channel_id: voice.channel.id,
|
||||||
|
self_mute: true,//todo
|
||||||
|
self_deaf: false,//todo
|
||||||
|
self_video: false,//What is this? I have some guesses
|
||||||
|
flags: 2//?????
|
||||||
|
},
|
||||||
|
op:4
|
||||||
|
}));
|
||||||
|
if(this.waitingForVoice){
|
||||||
|
this.waitingForVoice(undefined);
|
||||||
|
}
|
||||||
|
return await new Promise<voiceupdate|undefined>((res)=>{this.waitingForVoice=res;})
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
heartbeat_interval: number = 0;
|
heartbeat_interval: number = 0;
|
||||||
updateChannel(json: channeljson): void{
|
updateChannel(json: channeljson): void{
|
||||||
const guild = this.guildids.get(json.guild_id);
|
const guild = this.guildids.get(json.guild_id);
|
||||||
|
@ -1167,18 +1193,18 @@ class Localuser{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const sounds = Voice.sounds;
|
const sounds = AVoice.sounds;
|
||||||
tas
|
tas
|
||||||
.addSelect(
|
.addSelect(
|
||||||
"Notification sound:",
|
"Notification sound:",
|
||||||
_=>{
|
_=>{
|
||||||
Voice.setNotificationSound(sounds[_]);
|
AVoice.setNotificationSound(sounds[_]);
|
||||||
},
|
},
|
||||||
sounds,
|
sounds,
|
||||||
{ defaultIndex: sounds.indexOf(Voice.getNotificationSound()) }
|
{ defaultIndex: sounds.indexOf(AVoice.getNotificationSound()) }
|
||||||
)
|
)
|
||||||
.watchForChange(_=>{
|
.watchForChange(_=>{
|
||||||
Voice.noises(sounds[_]);
|
AVoice.noises(sounds[_]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,12 +116,12 @@ function setDefaults(){
|
||||||
setDefaults();
|
setDefaults();
|
||||||
class Specialuser{
|
class Specialuser{
|
||||||
serverurls: {
|
serverurls: {
|
||||||
api: string;
|
api: string;
|
||||||
cdn: string;
|
cdn: string;
|
||||||
gateway: string;
|
gateway: string;
|
||||||
wellknown: string;
|
wellknown: string;
|
||||||
login: string;
|
login: string;
|
||||||
};
|
};
|
||||||
email: string;
|
email: string;
|
||||||
token: string;
|
token: string;
|
||||||
loggedin;
|
loggedin;
|
||||||
|
|
403
src/webpage/voice.ts
Normal file
403
src/webpage/voice.ts
Normal file
|
@ -0,0 +1,403 @@
|
||||||
|
import { Channel } from "./channel.js";
|
||||||
|
import { sdpback, webRTCSocket } from "./jsontypes.js";
|
||||||
|
|
||||||
|
class Voice{
|
||||||
|
owner:Channel;
|
||||||
|
static url?:string;
|
||||||
|
static gotUrl:()=>void;
|
||||||
|
static geturl=new Promise<void>(res=>{this.gotUrl=res})
|
||||||
|
get channel(){
|
||||||
|
return this.owner;
|
||||||
|
}
|
||||||
|
get guild(){
|
||||||
|
return this.owner.owner;
|
||||||
|
}
|
||||||
|
get localuser(){
|
||||||
|
return this.owner.localuser;
|
||||||
|
}
|
||||||
|
get info(){
|
||||||
|
return this.owner.info;
|
||||||
|
}
|
||||||
|
constructor(owner:Channel){
|
||||||
|
this.owner=owner;
|
||||||
|
}
|
||||||
|
pc?:RTCPeerConnection;
|
||||||
|
ws?:WebSocket;
|
||||||
|
timeout:number=30000;
|
||||||
|
interval:NodeJS.Timeout=0 as unknown as NodeJS.Timeout;
|
||||||
|
time:number=0;
|
||||||
|
seq:number=0;
|
||||||
|
sendAlive(){
|
||||||
|
if(this.ws){
|
||||||
|
this.ws.send(JSON.stringify({ op: 3,d:10}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly users= new Map<number,string>();
|
||||||
|
packet(message:MessageEvent){
|
||||||
|
const data=message.data
|
||||||
|
if(typeof data === "string"){
|
||||||
|
const json:webRTCSocket = JSON.parse(data);
|
||||||
|
switch(json.op){
|
||||||
|
case 2:
|
||||||
|
this.startWebRTC();
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
this.continueWebRTC(json);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
this.time=json.d.t;
|
||||||
|
setTimeout(this.sendAlive.bind(this), this.timeout);
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
this.timeout=json.d.heartbeat_interval;
|
||||||
|
setTimeout(this.sendAlive.bind(this), 1000);
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
this.users.set(json.d.audio_ssrc,json.d.user_id);
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offer?:string;
|
||||||
|
cleanServerSDP(sdp:string,bundle1:string,bundle2:string):string{
|
||||||
|
if(!this.offer) throw new Error("Offer is missing :P");
|
||||||
|
let cline:string|undefined;
|
||||||
|
console.log(sdp);
|
||||||
|
for(const line of sdp.split("\n")){
|
||||||
|
if(line.startsWith("c=")){
|
||||||
|
cline=line;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!cline) throw new Error("c line wasn't found");
|
||||||
|
const parsed1=Voice.parsesdp(sdp).medias[0];
|
||||||
|
//const parsed2=Voice.parsesdp(this.offer);
|
||||||
|
const rtcport=(parsed1.atr.get("rtcp") as Set<string>).values().next().value as string;
|
||||||
|
const ICE_UFRAG=(parsed1.atr.get("ice-ufrag") as Set<string>).values().next().value as string;
|
||||||
|
const ICE_PWD=(parsed1.atr.get("ice-pwd") as Set<string>).values().next().value as string;
|
||||||
|
const FINGERPRINT=(parsed1.atr.get("fingerprint") as Set<string>).values().next().value as string;
|
||||||
|
const candidate=(parsed1.atr.get("candidate") as Set<string>).values().next().value as string;
|
||||||
|
let build=`v=0\r
|
||||||
|
o=- 1420070400000 0 IN IP4 127.0.0.1\r
|
||||||
|
s=-\r
|
||||||
|
t=0 0\r
|
||||||
|
a=msid-semantic: WMS *\r
|
||||||
|
a=group:BUNDLE ${bundle1} ${bundle2}\r
|
||||||
|
m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF 111\r
|
||||||
|
${cline}\r
|
||||||
|
a=rtpmap:111 opus/48000/2\r
|
||||||
|
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r
|
||||||
|
a=rtcp:${rtcport}\r
|
||||||
|
a=rtcp-fb:111 transport-cc\r
|
||||||
|
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r
|
||||||
|
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n
|
||||||
|
a=setup:passive\r
|
||||||
|
a=mid:${bundle1}\r
|
||||||
|
a=maxptime:60\r
|
||||||
|
a=inactive\r
|
||||||
|
a=ice-ufrag:${ICE_UFRAG}\r
|
||||||
|
a=ice-pwd:${ICE_PWD}\r
|
||||||
|
a=fingerprint:${FINGERPRINT}\r
|
||||||
|
a=candidate:${candidate}\r
|
||||||
|
a=rtcp-mux\r
|
||||||
|
m=video ${rtcport} UDP/TLS/RTP/SAVPF 102 103\r
|
||||||
|
${cline}\r
|
||||||
|
a=rtpmap:102 H264/90000\r
|
||||||
|
a=rtpmap:103 rtx/90000\r
|
||||||
|
a=fmtp:102 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r
|
||||||
|
a=fmtp:103 apt=102\r
|
||||||
|
a=rtcp:${rtcport}\r
|
||||||
|
a=rtcp-fb:102 ccm fir\r
|
||||||
|
a=rtcp-fb:102 nack\r
|
||||||
|
a=rtcp-fb:102 nack pli\r
|
||||||
|
a=rtcp-fb:102 goog-remb\r
|
||||||
|
a=rtcp-fb:102 transport-cc\r
|
||||||
|
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time/r/n
|
||||||
|
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n
|
||||||
|
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r
|
||||||
|
a=extmap:13 urn:3gpp:video-orientation\r
|
||||||
|
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay/r/na=setup:passive/r/n
|
||||||
|
a=mid:${bundle2}\r
|
||||||
|
a=inactive\r
|
||||||
|
a=ice-ufrag:${ICE_UFRAG}\r
|
||||||
|
a=ice-pwd:${ICE_PWD}\r
|
||||||
|
a=fingerprint:${FINGERPRINT}\r
|
||||||
|
a=candidate:${candidate}\r
|
||||||
|
a=rtcp-mux\r
|
||||||
|
`;
|
||||||
|
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
counter?:string;
|
||||||
|
negotationneeded(){
|
||||||
|
if(this.pc&&this.offer){
|
||||||
|
const pc=this.pc;
|
||||||
|
pc.addEventListener("negotiationneeded", async ()=>{
|
||||||
|
this.offer=(await pc.createOffer({
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: true
|
||||||
|
})).sdp;
|
||||||
|
await pc.setLocalDescription({sdp:this.offer});
|
||||||
|
const ld=pc.localDescription;
|
||||||
|
if(!ld) throw new Error("localDescription isn't defined");
|
||||||
|
if(!this.counter) throw new Error("localDescription isn't defined");
|
||||||
|
const counter=this.counter;
|
||||||
|
const parsed = Voice.parsesdp(ld.sdp);
|
||||||
|
const group=parsed.atr.get("group");
|
||||||
|
if(!group) throw new Error("group isn't in sdp");
|
||||||
|
const groupings=(group.entries().next().value as [string, string])[0].split(" ") as [string,string,string];
|
||||||
|
groupings[2]=groupings[2].replace("\r","");
|
||||||
|
console.log(groupings);
|
||||||
|
const remote:{sdp:string,type:RTCSdpType}={sdp:this.cleanServerSDP(counter,groupings[1],groupings[2]),type:"answer"};
|
||||||
|
console.log(remote);
|
||||||
|
await pc.setRemoteDescription(remote);
|
||||||
|
const senders=this.senders.difference(this.ssrcMap);
|
||||||
|
for(const sender of senders){
|
||||||
|
for(const thing of (await sender.getStats())){
|
||||||
|
console.log(thing[1]);
|
||||||
|
if(thing[1].ssrc){
|
||||||
|
this.ssrcMap.set(sender,thing[1].ssrc);
|
||||||
|
this.makeOp12(sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(this.ssrcMap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async makeOp12(sender:RTCRtpSender){
|
||||||
|
if(this.ws){
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
op: 12,
|
||||||
|
d: {
|
||||||
|
audio_ssrc: this.ssrcMap.get(sender),
|
||||||
|
video_ssrc: 0,
|
||||||
|
rtx_ssrc: 0,
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
rid: "100",
|
||||||
|
ssrc: 0,//TODO
|
||||||
|
active: false,
|
||||||
|
quality: 100,
|
||||||
|
rtx_ssrc: 0,//TODO
|
||||||
|
max_bitrate: 2500000,//TODO
|
||||||
|
max_framerate: 0,//TODO
|
||||||
|
max_resolution: {
|
||||||
|
type: "fixed",
|
||||||
|
width: 0,//TODO
|
||||||
|
height: 0//TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senders:Set<RTCRtpSender>=new Set();
|
||||||
|
ssrcMap:Map<RTCRtpSender,string>=new Map();
|
||||||
|
async continueWebRTC(data:sdpback){
|
||||||
|
if(this.pc&&this.offer){
|
||||||
|
const pc=this.pc;
|
||||||
|
this.negotationneeded();
|
||||||
|
const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} );
|
||||||
|
for (const track of audioStream.getTracks())
|
||||||
|
{
|
||||||
|
//Add track
|
||||||
|
const sender = pc.addTrack(track,audioStream);
|
||||||
|
this.senders.add(sender);
|
||||||
|
}
|
||||||
|
this.counter=data.d.sdp;
|
||||||
|
pc.ontrack = ({ streams: [stream] }) => console.log("got audio stream", stream);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async startWebRTC(){
|
||||||
|
const pc = new RTCPeerConnection();
|
||||||
|
this.pc=pc;
|
||||||
|
const offer = await pc.createOffer({
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: true
|
||||||
|
});
|
||||||
|
const sdp=offer.sdp;
|
||||||
|
this.offer=sdp;
|
||||||
|
|
||||||
|
if(!sdp){this.ws?.close();return;}
|
||||||
|
const parsed=Voice.parsesdp(sdp);
|
||||||
|
const video=new Map<string,[number,number]>();
|
||||||
|
const audio=new Map<string,number>();
|
||||||
|
let cur:[number,number]|undefined;
|
||||||
|
let i=0;
|
||||||
|
for(const thing of parsed.medias){
|
||||||
|
try{
|
||||||
|
if(thing.media==="video"){
|
||||||
|
const rtpmap=thing.atr.get("rtpmap");
|
||||||
|
if(!rtpmap) continue;
|
||||||
|
for(const codecpair of rtpmap){
|
||||||
|
|
||||||
|
const [port, codec]=codecpair.split(" ");
|
||||||
|
if(cur&&codec.split("/")[0]==="rtx"){
|
||||||
|
cur[1]=Number(port);
|
||||||
|
cur=undefined;
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if(video.has(codec.split("/")[0])) continue;
|
||||||
|
cur=[Number(port),-1];
|
||||||
|
video.set(codec.split("/")[0],cur);
|
||||||
|
}
|
||||||
|
}else if(thing.media==="audio"){
|
||||||
|
const rtpmap=thing.atr.get("rtpmap");
|
||||||
|
if(!rtpmap) continue;
|
||||||
|
for(const codecpair of rtpmap){
|
||||||
|
const [port, codec]=codecpair.split(" ");
|
||||||
|
if(audio.has(codec.split("/")[0])) { continue};
|
||||||
|
audio.set(codec.split("/")[0],Number(port));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}finally{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const codecs:{
|
||||||
|
name: string,
|
||||||
|
type: "video"|"audio",
|
||||||
|
priority: number,
|
||||||
|
payload_type: number,
|
||||||
|
rtx_payload_type: number|null
|
||||||
|
}[]=[];
|
||||||
|
const include=new Set<string>();
|
||||||
|
const audioAlloweds=new Map([["opus",{priority:1000,}]]);
|
||||||
|
for(const thing of audio){
|
||||||
|
if(audioAlloweds.has(thing[0])){
|
||||||
|
include.add(thing[0]);
|
||||||
|
codecs.push({
|
||||||
|
name:thing[0],
|
||||||
|
type:"audio",
|
||||||
|
priority:audioAlloweds.get(thing[0])?.priority as number,
|
||||||
|
payload_type:thing[1],
|
||||||
|
rtx_payload_type:null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const videoAlloweds=new Map([["H264",{priority:1000}],["VP8",{priority:2000}],["VP9",{priority:3000}]]);
|
||||||
|
for(const thing of video){
|
||||||
|
if(videoAlloweds.has(thing[0])){
|
||||||
|
include.add(thing[0]);
|
||||||
|
codecs.push({
|
||||||
|
name:thing[0],
|
||||||
|
type:"video",
|
||||||
|
priority:videoAlloweds.get(thing[0])?.priority as number,
|
||||||
|
payload_type:thing[1][0],
|
||||||
|
rtx_payload_type:thing[1][1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sendsdp="a=extmap-allow-mixed";
|
||||||
|
let first=true;
|
||||||
|
for(const media of parsed.medias){
|
||||||
|
|
||||||
|
for(const thing of first?["ice-ufrag","ice-pwd","ice-options","fingerprint","extmap","rtpmap"]:["extmap","rtpmap"]){
|
||||||
|
const thing2=media.atr.get(thing);
|
||||||
|
if(!thing2) continue;
|
||||||
|
for(const thing3 of thing2){
|
||||||
|
if(thing === "rtpmap"){
|
||||||
|
const name=thing3.split(" ")[1].split("/")[0];
|
||||||
|
if(include.has(name)){
|
||||||
|
include.delete(name);
|
||||||
|
}else{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendsdp+=`\na=${thing}:${thing3}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first=false;
|
||||||
|
}
|
||||||
|
if(this.ws){
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
d:{
|
||||||
|
codecs,
|
||||||
|
protocol:"webrtc",
|
||||||
|
data:sendsdp,
|
||||||
|
sdp:sendsdp
|
||||||
|
},
|
||||||
|
op:1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static parsesdp(sdp:string){
|
||||||
|
let currentA=new Map<string,Set<string>>();
|
||||||
|
const out:{version?:number,medias:{media:string,port:number,proto:string,ports:number[],atr:Map<string,Set<string>>}[],atr:Map<string,Set<string>>}={medias:[],atr:currentA};
|
||||||
|
for(const line of sdp.split("\n")){
|
||||||
|
const [code,setinfo]=line.split("=");
|
||||||
|
switch(code){
|
||||||
|
case "v":
|
||||||
|
out.version=Number(setinfo);
|
||||||
|
break;
|
||||||
|
case "o":
|
||||||
|
case "s":
|
||||||
|
case "t":
|
||||||
|
break;
|
||||||
|
case "m":
|
||||||
|
currentA=new Map();
|
||||||
|
const [media,port,proto,...ports]=setinfo.split(" ");
|
||||||
|
const portnums=ports.map(Number);
|
||||||
|
out.medias.push({media,port:Number(port),proto,ports:portnums,atr:currentA});
|
||||||
|
break;
|
||||||
|
case "a":
|
||||||
|
const [key, ...value] = setinfo.split(":");
|
||||||
|
if(!currentA.has(key)){
|
||||||
|
currentA.set(key,new Set());
|
||||||
|
}
|
||||||
|
currentA.get(key)?.add(value.join(":"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
async join(){
|
||||||
|
console.warn("Joining");
|
||||||
|
const json = await this.localuser.joinVoice(this);
|
||||||
|
if(!json) return;
|
||||||
|
if(!Voice.url){
|
||||||
|
await Voice.geturl;
|
||||||
|
}
|
||||||
|
if(this.localuser.currentVoice!==this){return}
|
||||||
|
const ws=new WebSocket("ws://"+Voice.url as string);
|
||||||
|
this.ws=ws;
|
||||||
|
ws.addEventListener("message",(m)=>{
|
||||||
|
this.packet(m);
|
||||||
|
})
|
||||||
|
await new Promise<void>(res=>{
|
||||||
|
ws.addEventListener("open",()=>{
|
||||||
|
res()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"op": 0,
|
||||||
|
"d": {
|
||||||
|
server_id: this.guild.id,
|
||||||
|
user_id: json.d.user_id,
|
||||||
|
session_id: json.d.session_id,
|
||||||
|
token: json.d.token,
|
||||||
|
video: false,
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
rid: "100",
|
||||||
|
quality: 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
/*
|
||||||
|
const pc=new RTCPeerConnection();
|
||||||
|
this.pc=pc;
|
||||||
|
//pc.setRemoteDescription({sdp:json.d.token,type:""})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export {Voice};
|
Loading…
Add table
Add a link
Reference in a new issue