diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..7975f3e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,64 @@
+# Skald Project Instructions
+
+Skald is a hard fork of Galene into an audio-only, hall-based conferencing
+server.
+
+## Direction
+
+- Treat Skald as incompatible with Galene unless compatibility is explicitly
+ requested.
+- Do not add legacy Galene routes, config fallbacks, protocol shims, migration
+ behavior, or compatibility aliases by default.
+- Replace Galene branding and group/room terminology with Skald and hall
+ terminology everywhere practical.
+- Apply renames and removals thoroughly. When changing a concept, route,
+ command, UI label, config key, file name, or behavior, search for every
+ related surface and update even small details such as tests, examples, help
+ text, comments, docs, error messages, CSS selectors, and generated/static
+ asset references.
+- Remove video behavior rather than hiding it.
+- Reject incoming video intentionally.
+- Recordings should become single mixed Ogg Opus audio files.
+
+## Workflow
+
+- Use `todo.txt` as the source of truth for staged work.
+- Preserve the todo status markers:
+ - `-` means open.
+ - `+` means implemented, needs testing.
+ - `X` means tested and done.
+- Work in small batches and update `todo.txt` only for items actually completed
+ or tested.
+- Before beginning a new phase or section, review all earlier `+` and `X`
+ sections in `todo.txt`; confirm their status still matches the code, tests,
+ docs, and user-facing surfaces, and downgrade stale `X` markers when evidence
+ no longer supports them.
+- Keep changes scoped to the current batch. Do not mix in unrelated cleanup.
+- Before behavior changes, inspect the current code path and tests rather than
+ assuming Galene behavior still applies.
+
+## Verification
+
+- Run the narrowest useful verification for each change.
+- For Go changes, prefer focused package tests first, then broader
+ `go test ./...` when the change crosses packages.
+- For frontend changes, verify renamed assets load and keyboard-accessible
+ controls still work where practical.
+- For shell script changes, run `shellcheck`.
+- If verification is not possible, state that plainly.
+
+## Accessibility
+
+- The browser UI must be fully screen-reader accessible, not merely visually
+ simplified.
+- Dynamic conference events must be announced through appropriate live regions
+ so screen readers read important changes automatically. This includes chat
+ messages, participant joins and leaves, raised hands, recording state changes,
+ moderation actions, connection status changes, and errors that require user
+ attention.
+- Use polite live regions for routine updates and assertive live regions only
+ for urgent or blocking errors.
+- Avoid duplicate announcements when updating visible text and live-region text.
+- The browser UI must be audio-only and keyboard accessible.
+- Preserve labels or accessible names when removing or reworking controls.
+- Do not rely on visual layout, sound, or color alone to communicate state.
diff --git a/codecs/codecs.go b/codecs/codecs.go
index e4849cd..5d6b6b0 100644
--- a/codecs/codecs.go
+++ b/codecs/codecs.go
@@ -2,312 +2,30 @@ package codecs
import (
"errors"
- "strings"
-
- "github.com/pion/rtp"
- "github.com/pion/rtp/codecs"
)
var errTruncated = errors.New("truncated packet")
-// Keyframe determines if packet is the start of a keyframe.
-// It returns (true, true) if that is the case, (false, true) if that is
-// definitely not the case, and (false, false) if the information cannot
-// be determined.
-func Keyframe(codec string, packet *rtp.Packet) (bool, bool) {
- if strings.EqualFold(codec, "video/vp8") {
- var vp8 codecs.VP8Packet
- _, err := vp8.Unmarshal(packet.Payload)
- if err != nil || len(vp8.Payload) < 1 {
- return false, false
- }
-
- if vp8.S != 0 && vp8.PID == 0 && (vp8.Payload[0]&0x1) == 0 {
- return true, true
- }
- return false, true
- } else if strings.EqualFold(codec, "video/vp9") {
- var vp9 codecs.VP9Packet
- _, err := vp9.Unmarshal(packet.Payload)
- if err != nil || len(vp9.Payload) < 1 {
- return false, false
- }
- if !vp9.B {
- return false, true
- }
-
- if (vp9.Payload[0] & 0xc0) != 0x80 {
- return false, false
- }
-
- profile := (vp9.Payload[0] >> 4) & 0x3
- if profile != 3 {
- return (vp9.Payload[0] & 0xC) == 0, true
- }
- return (vp9.Payload[0] & 0x6) == 0, true
- } else if strings.EqualFold(codec, "video/av1") {
- if len(packet.Payload) < 2 {
- return false, true
- }
- // Z=0, N=1
- if (packet.Payload[0] & 0x88) != 0x08 {
- return false, true
- }
- w := (packet.Payload[0] & 0x30) >> 4
-
- getObu := func(data []byte, last bool) ([]byte, int, bool) {
- if last {
- return data, len(data), false
- }
- offset := 0
- length := 0
- for {
- if len(data) <= offset {
- return nil, offset, offset > 0
- }
- if offset >= 4 {
- return nil, offset, true
- }
- l := data[offset]
- length |= int(l&0x7f) << (offset * 7)
- offset++
- if (l & 0x80) == 0 {
- break
- }
- }
- if len(data) < offset+length {
- return data[offset:], len(data), true
- }
- return data[offset : offset+length],
- offset + length, false
- }
- offset := 1
- i := 0
- for {
- obu, length, truncated :=
- getObu(packet.Payload[offset:], int(w) == i+1)
- if len(obu) < 1 {
- return false, false
- }
- tpe := (obu[0] & 0x38) >> 3
- switch i {
- case 0:
- // OBU_SEQUENCE_HEADER
- if tpe != 1 {
- return false, true
- }
- default:
- // OBU_FRAME_HEADER or OBU_FRAME
- if tpe == 3 || tpe == 6 {
- if len(obu) < 2 {
- return false, false
- }
- // show_existing_frame == 0
- if (obu[1] & 0x80) != 0 {
- return false, true
- }
- // frame_type == KEY_FRAME
- return (obu[1] & 0x60) == 0, true
- }
- }
- if truncated || i >= int(w) {
- // the first frame header is in a second
- // packet, give up.
- return false, false
- }
- offset += length
- i++
- }
- } else if strings.EqualFold(codec, "video/h264") {
- if len(packet.Payload) < 1 {
- return false, false
- }
- nalu := packet.Payload[0] & 0x1F
- if nalu == 0 {
- // reserved
- return false, false
- } else if nalu <= 23 {
- // simple NALU
- return nalu == 7, true
- } else if nalu == 24 || nalu == 25 || nalu == 26 || nalu == 27 {
- // STAP-A, STAP-B, MTAP16 or MTAP24
- i := 1
- if nalu == 25 || nalu == 26 || nalu == 27 {
- // skip DON
- i += 2
- }
- for i < len(packet.Payload) {
- if i+2 > len(packet.Payload) {
- return false, false
- }
- length := uint16(packet.Payload[i])<<8 |
- uint16(packet.Payload[i+1])
- i += 2
- if i+int(length) > len(packet.Payload) {
- return false, false
- }
- offset := 0
- if nalu == 26 {
- offset = 3
- } else if nalu == 27 {
- offset = 4
- }
- if offset >= int(length) {
- return false, false
- }
- n := packet.Payload[i+offset] & 0x1F
- if n == 7 {
- return true, true
- } else if n >= 24 {
- // is this legal?
- return false, false
- }
- i += int(length)
- }
- if i == len(packet.Payload) {
- return false, true
- }
- return false, false
- } else if nalu == 28 || nalu == 29 {
- // FU-A or FU-B
- if len(packet.Payload) < 2 {
- return false, false
- }
- if (packet.Payload[1] & 0x80) == 0 {
- // not a starting fragment
- return false, true
- }
- return (packet.Payload[1]&0x1F == 7), true
- }
- return false, false
- }
- return false, false
-}
-
-func KeyframeDimensions(codec string, packet *rtp.Packet) (uint32, uint32) {
- if strings.EqualFold(codec, "video/vp8") {
- var vp8 codecs.VP8Packet
- _, err := vp8.Unmarshal(packet.Payload)
- if err != nil {
- return 0, 0
- }
- if len(vp8.Payload) < 10 {
- return 0, 0
- }
- raw := uint32(vp8.Payload[6]) | uint32(vp8.Payload[7])<<8 |
- uint32(vp8.Payload[8])<<16 | uint32(vp8.Payload[9])<<24
- width := raw & 0x3FFF
- height := (raw >> 16) & 0x3FFF
- return width, height
- } else if strings.EqualFold(codec, "video/vp9") {
- if packet == nil {
- return 0, 0
- }
- var vp9 codecs.VP9Packet
- _, err := vp9.Unmarshal(packet.Payload)
- if err != nil {
- return 0, 0
- }
- if !vp9.V {
- return 0, 0
- }
- w := uint32(0)
- h := uint32(0)
- for i := range vp9.Width {
- if i >= len(vp9.Height) {
- break
- }
- if w < uint32(vp9.Width[i]) {
- w = uint32(vp9.Width[i])
- }
- if h < uint32(vp9.Height[i]) {
- h = uint32(vp9.Height[i])
- }
- }
- return w, h
- } else {
- return 0, 0
- }
-}
-
+// Flags holds parsed RTP header flags.
type Flags struct {
- Seqno uint16
- Marker bool
- Start bool
- End bool
- Keyframe bool
- Pid uint16 // only returned for VP8
- Tid uint8
- Sid uint8
- TidUpSync bool
- SidUpSync bool
- SidNonReference bool
- Discardable bool
+ Seqno uint16
+ Pid uint16
+ Marker bool
}
+// PacketFlags extracts basic RTP header flags from a raw packet buffer.
func PacketFlags(codec string, buf []byte) (Flags, error) {
if len(buf) < 4 {
return Flags{}, errTruncated
}
var flags Flags
-
flags.Seqno = (uint16(buf[2]) << 8) | uint16(buf[3])
flags.Marker = (buf[1] & 0x80) != 0
-
- if strings.EqualFold(codec, "video/vp8") {
- var packet rtp.Packet
- err := packet.Unmarshal(buf)
- if err != nil {
- return flags, err
- }
- var vp8 codecs.VP8Packet
- _, err = vp8.Unmarshal(packet.Payload)
- if err != nil {
- return flags, err
- }
- flags.Start = vp8.S != 0 && vp8.PID == 0
- flags.End = packet.Marker
- flags.Keyframe = vp8.S != 0 && vp8.PID == 0 &&
- len(vp8.Payload) > 0 && (vp8.Payload[0]&0x1) == 0
- flags.Pid = vp8.PictureID
- flags.Tid = vp8.TID
- flags.TidUpSync = flags.Keyframe || vp8.Y == 1
- flags.SidUpSync = flags.Keyframe
- flags.Discardable = vp8.N == 1
- return flags, nil
- } else if strings.EqualFold(codec, "video/vp9") {
- var packet rtp.Packet
- err := packet.Unmarshal(buf)
- if err != nil {
- return flags, err
- }
- var vp9 codecs.VP9Packet
- _, err = vp9.Unmarshal(packet.Payload)
- if err != nil {
- return flags, err
- }
- flags.Start = vp9.B
- flags.End = vp9.E
- if vp9.B && len(vp9.Payload) > 0 &&
- (vp9.Payload[0]&0xc0) == 0x80 {
- profile := (vp9.Payload[0] >> 4) & 0x3
- if profile != 3 {
- flags.Keyframe = (vp9.Payload[0] & 0xC) == 0
- } else {
- flags.Keyframe = (vp9.Payload[0] & 0x6) == 0
- }
- }
- flags.Tid = vp9.TID
- flags.Sid = vp9.SID
- flags.TidUpSync = flags.Keyframe || vp9.U
- flags.SidUpSync = flags.Keyframe || !vp9.P
- flags.SidNonReference = (packet.Payload[0] & 0x01) != 0
- return flags, nil
- }
return flags, nil
}
+// RewritePacket rewrites the sequence number and marker bit in a raw RTP packet.
func RewritePacket(codec string, data []byte, setMarker bool, seqno uint16, delta uint16) error {
if len(data) < 12 {
return errTruncated
@@ -337,39 +55,5 @@ func RewritePacket(codec string, data []byte, setMarker bool, seqno uint16, delt
}
}
- // only rewrite PID for VP8.
- if strings.EqualFold(codec, "video/vp8") {
- x := (data[offset] & 0x80) != 0
- if !x {
- return nil
- }
- offset++
- if len(data) <= offset {
- return errTruncated
- }
- i := (data[offset] & 0x80) != 0
- if !i {
- return nil
- }
- offset++
- if len(data) <= offset {
- return errTruncated
- }
- m := (data[offset] & 0x80) != 0
- if m {
- if len(data) <= offset+1 {
- return errTruncated
- }
- pid := (uint16(data[offset]&0x7F) << 8) |
- uint16(data[offset+1])
- pid = (pid + delta) & 0x7FFF
- data[offset] = 0x80 | byte((pid>>8)&0x7F)
- data[offset+1] = byte(pid & 0xFF)
- } else {
- data[offset] = (data[offset] + uint8(delta)) & 0x7F
- }
- return nil
- }
-
return nil
}
diff --git a/codecs/codecs_test.go b/codecs/codecs_test.go
index 68521c8..4a5d413 100644
--- a/codecs/codecs_test.go
+++ b/codecs/codecs_test.go
@@ -1,249 +1,37 @@
package codecs
import (
- "bytes"
"testing"
-
- "github.com/pion/rtp"
)
-func TestVP8Keyframe(t *testing.T) {
- ps := [][]byte{
- {
-
- 0x80, 0xe0, 0x71, 0x3e, 0x5d, 0x6f, 0x3c, 0xc5,
- 0x75, 0xc, 0x80, 0x96, 0x90, 0x80, 0xb0, 0x4c,
- 0x90, 0x2, 0x0, 0x9d, 0x1, 0x2a, 0x10, 0x0, 0x10,
- 0x0, 0x39, 0x3, 0x0, 0x0, 0x1c, 0x22, 0x16, 0x16,
- 0x22, 0x66, 0x12, 0x20, 0x4, 0x90, 0x40, 0x4e,
- 0x9e, 0x8d, 0xe9, 0x40, 0xfe, 0xff, 0xab, 0x59,
- 0x72, 0x30, 0xd1, 0xaf, 0xe4, 0x6a, 0x11, 0x3,
- 0xfd, 0x15, 0xe9, 0x2, 0x2e, 0xdf, 0xd9, 0xd1,
- 0xb8, 0x0, 0x0,
- },
- {
- 0x80, 0x6f, 0x61, 0x8f, 0xd5, 0x36, 0xdc, 0x15,
- 0x1b, 0x4a, 0xb5, 0x29, 0x78, 0x9, 0xa1, 0x93,
- 0xa0, 0x5b, 0xd8, 0xf1, 0xde, 0x87, 0x23, 0x5a,
- 0xb9, 0x19, 0x97, 0xb7, 0xbd, 0xbf, 0xf7, 0x6e,
- 0xad, 0x82, 0xc4, 0x70, 0x1c, 0xc9, 0x3a, 0xb4,
- 0x1f, 0x13, 0x45, 0xb5, 0xf1, 0x0, 0xa5, 0xa5,
- 0xa9, 0xd0, 0xa5, 0xdf, 0x67, 0x88, 0x26, 0x30,
- 0x32,
- },
+func TestPacketFlags(t *testing.T) {
+ buf := []byte{0x80, 0x80, 0x01, 0x02, 0x03, 0x04}
+ flags, err := PacketFlags("audio/opus", buf)
+ if err != nil {
+ t.Fatalf("PacketFlags: %v", err)
}
-
- var packet rtp.Packet
-
- for i, p := range ps {
- err := packet.Unmarshal(p)
- if err != nil {
- t.Errorf("Unmarshal(p%v): %v", i, err)
- }
-
- kf, kfKnown := Keyframe("video/vp8", &packet)
- if kf != (i == 0) || !kfKnown {
- t.Errorf("Keyframe(p%v): %v %v", i, kf, kfKnown)
- }
+ if flags.Seqno != 0x0102 {
+ t.Errorf("seqno %v", flags.Seqno)
+ }
+ if !flags.Marker {
+ t.Errorf("marker")
}
}
-func TestVP9Keyframe(t *testing.T) {
- ps := [][]byte{
- {
- 0x80, 0xe2, 0x6c, 0xb9, 0xcd, 0xa2, 0x77, 0x5c,
- 0xea, 0xf0, 0x14, 0xe9, 0x8f, 0xbd, 0x90, 0x18,
- 0x0, 0x10, 0x0, 0x10, 0x1, 0x4, 0x1, 0x82, 0x49,
- 0x83, 0x42, 0x0, 0x0, 0xf0, 0x0, 0xf4, 0x2, 0x38,
- 0x24, 0x1c, 0x18, 0x10, 0x0, 0x0, 0x20, 0x40, 0x0,
- 0x22, 0x9b, 0xff, 0xff, 0xd7, 0xe6, 0xc0, 0xa,
- 0xf2, 0x32, 0xd4, 0xdd, 0xa3, 0x69, 0xc6, 0xca,
- 0xd1, 0x50, 0xeb, 0x1c, 0x1, 0x50, 0x91, 0xf6,
- 0x64, 0xc7, 0x35, 0xe9, 0x0, 0xfe, 0x76, 0xb2,
- 0xb, 0x4d, 0xd7, 0x35, 0x23, 0xf3, 0x9f, 0x7f,
- 0x86, 0x37, 0xb9, 0x65, 0x3a, 0xf9, 0x66, 0xa0,
- 0x6a, 0xb2, 0x9b, 0xb3, 0x36, 0x5b, 0x47, 0xf2,
- 0x26, 0x5c, 0xe2, 0x23, 0x4f, 0xff, 0xff, 0xff,
- 0xfe, 0xc3, 0x49, 0x6b, 0x14, 0x58, 0x4d, 0xdc,
- 0xd8, 0xf5, 0x76, 0x81, 0x2e, 0xb3, 0x7f, 0xff,
- 0xfe, 0x18, 0xc8, 0xf8, 0x1b, 0xf6, 0xee, 0xc3,
- 0xc, 0x6f, 0x23, 0x34, 0x80,
- },
- {
- 0x80, 0xe2, 0x4a, 0xb5, 0x1a, 0x33, 0x3f, 0x7b,
- 0x9c, 0xda, 0x7b, 0xd0, 0x8d, 0xec, 0x14, 0x86,
- 0x0, 0x40, 0x92, 0x88, 0x2c, 0x50, 0x83, 0x30,
- 0x10, 0x1c, 0x6, 0x3, 0x0, 0x82, 0x99, 0x15, 0xc8,
- 0x0, 0x0, 0x0, 0x0, 0x18, 0x70, 0x0, 0x0, 0x4c,
- 0x4, 0xa0,
- },
+func TestRewritePacket(t *testing.T) {
+ buf := []byte{
+ 0x80, 0x00, 0x01, 0x02,
+ 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0a,
}
-
- var packet rtp.Packet
-
- for i, p := range ps {
- err := packet.Unmarshal(p)
- if err != nil {
- t.Errorf("Unmarshal(p%v): %v", i, err)
- }
-
- kf, kfKnown := Keyframe("video/vp9", &packet)
- if kf != (i == 0) || !kfKnown {
- t.Errorf("Keyframe(p%v): %v %v", i, kf, kfKnown)
- }
+ err := RewritePacket("audio/opus", buf, true, 0x0506, 0)
+ if err != nil {
+ t.Fatalf("RewritePacket: %v", err)
}
-}
-
-func TestH264Keyframe(t *testing.T) {
- ps := [][]byte{
- {
- 0x80, 0xe6, 0xf, 0xae, 0xfa, 0x86, 0x3b, 0x49,
- 0x59, 0xbd, 0x79, 0xe7, 0x78, 0x0, 0xc, 0x67,
- 0x42, 0xc0, 0xc, 0x8c, 0x8d, 0x4e, 0x40, 0x3c,
- 0x22, 0x11, 0xa8, 0x0, 0x4, 0x68, 0xce, 0x3c,
- 0x80, 0x0, 0x1a, 0x65, 0xb8, 0x0, 0x4, 0x0, 0x0,
- 0x9, 0xe3, 0x31, 0x40, 0x0, 0x46, 0x76, 0x38, 0x0,
- 0x8, 0x2, 0x47, 0x0, 0x2, 0x7f, 0x3f, 0x77, 0x6f,
- 0x67, 0x80,
- },
- {
-
- 0x80, 0xe6, 0xf, 0xaf, 0xfa, 0x86, 0x46, 0x89,
- 0x59, 0xbd, 0x79, 0xe7, 0x61, 0xe0, 0x0, 0x40,
- 0x0, 0xbe, 0x40, 0x9e, 0xa0,
- },
+ if buf[1] != 0x80 {
+ t.Errorf("marker not set")
}
-
- var packet rtp.Packet
-
- for i, p := range ps {
- err := packet.Unmarshal(p)
- if err != nil {
- t.Errorf("Unmarshal(p%v): %v", i, err)
- }
-
- kf, kfKnown := Keyframe("video/h264", &packet)
- if kf != (i == 0) || !kfKnown {
- t.Errorf("Keyframe(p%v): %v %v", i, kf, kfKnown)
- }
- }
-}
-
-var vp8 = []byte{
- 0x80, 0, 0, 42,
- 0, 0, 0, 0,
- 0, 0, 0, 0,
-
- 0x90, 0x80, 0x80, 57,
-
- 0, 0, 0, 0,
-}
-
-var emptyVP8 = []byte{
- 0x80, 0, 0, 42,
- 0, 0, 0, 0,
- 0, 0, 0, 0,
-
- 0x00,
-}
-
-func TestPacketFlagsVP8(t *testing.T) {
- buf := bytes.Clone(vp8)
- flags, err := PacketFlags("video/vp8", buf)
- if flags.Seqno != 42 || !flags.Start || flags.Pid != 57 ||
- flags.Sid != 0 || flags.Tid != 0 ||
- !flags.TidUpSync || flags.Discardable || err != nil {
- t.Errorf("Got %v, %v, %v, %v, %v, %v (%v)",
- flags.Seqno, flags.Start, flags.Pid, flags.Sid,
- flags.TidUpSync, flags.Discardable, err,
- )
- }
-}
-
-func TestEmptyPacketFlagsVP8(t *testing.T) {
- buf := bytes.Clone(emptyVP8)
- flags, err := PacketFlags("video/vp8", buf)
- if flags.Seqno != 42 || flags.Start ||
- flags.Sid != 0 || flags.Tid != 0 ||
- flags.TidUpSync || flags.Discardable || err != nil {
- t.Errorf("Got %v, %v, %v, %v, %v, %v (%v)",
- flags.Seqno, flags.Start, flags.Pid, flags.Sid,
- flags.TidUpSync, flags.Discardable, err,
- )
- }
-}
-
-func TestRewriteVP8(t *testing.T) {
- for i := uint16(0); i < 0x7fff; i++ {
- buf := bytes.Clone(vp8)
- err := RewritePacket("video/vp8", buf, true, i, i)
- if err != nil {
- t.Errorf("rewrite: %v", err)
- continue
- }
- flags, err := PacketFlags("video/vp8", buf)
- if err != nil || flags.Seqno != i ||
- flags.Pid != (57+i)&0x7FFF || !flags.Marker {
- t.Errorf("Expected %v %v, got %v %v (%v)",
- i, (57+i)&0x7FFF,
- flags.Seqno, flags.Pid, err)
- }
- }
-}
-
-func TestRewriteEmptyVP8(t *testing.T) {
- for i := uint16(0); i < 0x7fff; i++ {
- buf := bytes.Clone(emptyVP8)
- err := RewritePacket("video/vp8", buf, true, i, i)
- if err != nil {
- t.Errorf("rewrite: %v", err)
- continue
- }
- flags, err := PacketFlags("video/vp8", buf)
- if err != nil || flags.Seqno != i ||
- !flags.Marker {
- t.Errorf("Expected %v %v, got %v %v (%v)",
- i, (57+i)&0x7FFF,
- flags.Seqno, flags.Pid, err)
- }
- }
-}
-
-var vp9 = []byte{
- 0x80, 0, 0, 42,
- 0, 0, 0, 0,
- 0, 0, 0, 0,
-
- 0x88, 0x80, 57, 0,
-}
-
-func TestPacketFlagsVP9(t *testing.T) {
- buf := bytes.Clone(vp9)
- flags, err := PacketFlags("video/vp9", buf)
- if flags.Seqno != 42 || !flags.Start || flags.Pid != 0 ||
- flags.Sid != 0 || flags.Tid != 0 ||
- flags.TidUpSync || flags.Discardable || err != nil {
- t.Errorf("Got %v, %v, %v, %v, %v, %v (%v)",
- flags.Seqno, flags.Start, flags.Pid, flags.Sid,
- flags.TidUpSync, flags.Discardable, err,
- )
- }
-}
-
-func TestRewriteVP9(t *testing.T) {
- for i := uint16(0); i < 0x7fff; i++ {
- buf := bytes.Clone(vp9)
- err := RewritePacket("video/vp9", buf, true, i, i)
- if err != nil {
- t.Errorf("rewrite: %v", err)
- continue
- }
- flags, err := PacketFlags("video/vp9", buf)
- if err != nil || flags.Seqno != i ||
- flags.Pid != 0 || !flags.Marker {
- t.Errorf("Expected %v %v, got %v %v (%v)",
- i, 0,
- flags.Seqno, flags.Pid, err)
- }
+ if buf[2] != 0x05 || buf[3] != 0x06 {
+ t.Errorf("seqno %v %v", buf[2], buf[3])
}
}
diff --git a/conn/conn.go b/conn/conn.go
index c7ec0d5..f1a122e 100644
--- a/conn/conn.go
+++ b/conn/conn.go
@@ -28,7 +28,6 @@ type UpTrack interface {
// GetPacket fetches a recent packet. Returns 0 if the packet is
// not in cache, and, in that case, optionally schedules a NACK.
GetPacket(seqno uint16, result []byte, nack bool) uint16
- RequestKeyframe() error
}
// Type Down represents a connection in the server to client direction.
diff --git a/diskwriter/diskwriter.go b/diskwriter/diskwriter.go
index 1ecbc5b..91466c4 100644
--- a/diskwriter/diskwriter.go
+++ b/diskwriter/diskwriter.go
@@ -22,7 +22,6 @@ import (
"github.com/jech/samplebuilder"
- gcodecs "git.stormux.org/storm/skald/codecs"
"git.stormux.org/storm/skald/conn"
"git.stormux.org/storm/skald/hall"
"git.stormux.org/storm/skald/rtptime"
@@ -30,7 +29,6 @@ import (
const (
audioMaxLate = 32
- videoMaxLate = 256
)
var Directory string
@@ -82,7 +80,7 @@ func (client *Client) Data() map[string]interface{} {
return nil
}
-func (client *Client) PushClient(group, kind, id, username string, perms []string, data map[string]interface{}) error {
+func (client *Client) PushClient(hall, kind, id, username string, perms []string, data map[string]interface{}) error {
return nil
}
@@ -112,7 +110,7 @@ func (client *Client) Addr() net.Addr {
return nil
}
-func (client *Client) Joined(group, kind string) error {
+func (client *Client) Joined(hall, kind string) error {
return nil
}
@@ -173,16 +171,14 @@ type diskConn struct {
client *Client
directory string
username string
- hasVideo bool
- mu sync.Mutex
- file *os.File
- remote conn.Up
- tracks []*diskTrack
- width, height uint32
- lastWarning time.Time
- originLocal time.Time
- originRemote uint64
+ mu sync.Mutex
+ file *os.File
+ remote conn.Up
+ tracks []*diskTrack
+ lastWarning time.Time
+ originLocal time.Time
+ originRemote uint64
}
// called locked
@@ -315,14 +311,10 @@ type diskTrack struct {
remoteNTP uint64
remoteRTP uint32
-
- kfRequested time.Time
- lastKf time.Time
- savedKf *rtp.Packet
}
func newDiskConn(client *Client, directory string, up conn.Up, remoteTracks []conn.UpTrack) (*diskConn, error) {
- var audio, video conn.UpTrack
+ var audio conn.UpTrack
for _, remote := range remoteTracks {
codec := remote.Codec().MimeType
@@ -332,30 +324,16 @@ func newDiskConn(client *Client, directory string, up conn.Up, remoteTracks []co
} else {
client.hall.WallOps("Multiple audio tracks, recording just one")
}
- } else if strings.EqualFold(codec, "video/vp8") ||
- strings.EqualFold(codec, "video/vp9") ||
- strings.EqualFold(codec, "video/h264") {
- if video == nil || video.Label() == "l" {
- video = remote
- } else if remote.Label() != "l" {
- client.hall.WallOps("Multiple video tracks, recording just one")
- }
} else {
client.hall.WallOps("Unknown codec, " + codec + ", not recording")
}
}
- if video == nil && audio == nil {
+ if audio == nil {
return nil, errors.New("no usable tracks found")
}
- tracks := make([]conn.UpTrack, 0, 2)
- if audio != nil {
- tracks = append(tracks, audio)
- }
- if video != nil {
- tracks = append(tracks, video)
- }
+ tracks := []conn.UpTrack{audio}
_, username := up.User()
conn := diskConn{
@@ -374,24 +352,6 @@ func newDiskConn(client *Client, directory string, up conn.Up, remoteTracks []co
audioMaxLate,
&codecs.OpusPacket{}, codec.ClockRate,
)
- } else if strings.EqualFold(codec.MimeType, "video/vp8") {
- builder = samplebuilder.New(
- videoMaxLate,
- &codecs.VP8Packet{}, codec.ClockRate,
- )
- conn.hasVideo = true
- } else if strings.EqualFold(codec.MimeType, "video/vp9") {
- builder = samplebuilder.New(
- videoMaxLate, &codecs.VP9Packet{},
- codec.ClockRate,
- )
- conn.hasVideo = true
- } else if strings.EqualFold(codec.MimeType, "video/h264") {
- builder = samplebuilder.New(
- videoMaxLate, &codecs.H264Packet{},
- codec.ClockRate,
- )
- conn.hasVideo = true
} else {
// this shouldn't happen
return nil, errors.New(
@@ -406,8 +366,6 @@ func newDiskConn(client *Client, directory string, up conn.Up, remoteTracks []co
conn.tracks = append(conn.tracks, track)
}
- // Only do this after all tracks have been added to conn, to avoid
- // racing on hasVideo.
for _, t := range conn.tracks {
err := t.remote.AddLocal(t)
if err != nil {
@@ -454,7 +412,7 @@ func (t *diskTrack) Write(buf []byte) (int, error) {
fetch(t, lastSeqno+i)
}
} else {
- requestKeyframe(t)
+
}
t.lastSeqno = some(uint32(p.SequenceNumber))
} else {
@@ -462,7 +420,7 @@ func (t *diskTrack) Write(buf []byte) (int, error) {
count := lastSeqno - p.SequenceNumber
if count >= 512 {
t.lastSeqno = none
- requestKeyframe(t)
+
}
}
} else {
@@ -491,41 +449,14 @@ func fetch(t *diskTrack, seqno uint16) {
t.writeRTP(p)
}
-func requestKeyframe(t *diskTrack) {
- now := time.Now()
- if now.Sub(t.kfRequested) > 500*time.Millisecond {
- t.remote.RequestKeyframe()
- t.kfRequested = now
- }
-}
-
// writeRTP writes the packet without fetching lost packets
// Called locked.
func (t *diskTrack) writeRTP(p *rtp.Packet) error {
- codec := t.remote.Codec().MimeType
- if len(codec) > 6 && strings.EqualFold(codec[:6], "video/") {
- kf, _ := gcodecs.Keyframe(codec, p)
- if kf {
- t.savedKf = p
- t.lastKf = time.Now()
- if !valid(t.origin) {
- t.setOrigin(
- p.Timestamp, time.Now(),
- t.remote.Codec().ClockRate,
- )
- }
- } else if time.Since(t.lastKf) > 4*time.Second {
- requestKeyframe(t)
- }
- }
-
if !valid(t.origin) {
- if !t.conn.hasVideo || !t.conn.originLocal.Equal(time.Time{}) {
- t.setOrigin(
- p.Timestamp, time.Now(),
- t.remote.Codec().ClockRate,
- )
- }
+ t.setOrigin(
+ p.Timestamp, time.Now(),
+ t.remote.Codec().ClockRate,
+ )
}
t.builder.Push(p)
@@ -537,8 +468,6 @@ func (t *diskTrack) writeRTP(p *rtp.Packet) error {
// samples will be flushed even if they are preceded by incomplete
// samples.
func (t *diskTrack) writeBuffered(force bool) error {
- codec := t.remote.Codec().MimeType
-
for {
var sample *media.Sample
var ts uint32
@@ -561,39 +490,13 @@ func (t *diskTrack) writeBuffered(force bool) error {
t.conn.close()
}
- var keyframe bool
- if len(codec) > 6 && strings.EqualFold(codec[:6], "video/") {
- if t.savedKf == nil {
- keyframe = false
- } else {
- keyframe = (ts == t.savedKf.Timestamp)
- }
-
- if keyframe {
- w, h := gcodecs.KeyframeDimensions(
- codec, t.savedKf,
+ if t.writer == nil {
+ err := t.conn.initWriter(t, ts)
+ if err != nil {
+ t.conn.warn(
+ "Write to disk " + err.Error(),
)
- err := t.conn.initWriter(w, h, t, ts)
- if err != nil {
- t.conn.warn(
- "Write to disk " + err.Error(),
- )
- return err
- }
- }
- } else {
- keyframe = true
- if t.writer == nil {
- if !t.conn.hasVideo {
- err := t.conn.initWriter(0, 0, t, ts)
- if err != nil {
- t.conn.warn(
- "Write to disk " +
- err.Error(),
- )
- return err
- }
- }
+ return err
}
}
@@ -608,6 +511,7 @@ func (t *diskTrack) writeBuffered(force bool) error {
tm := (ts - value(t.origin)) /
(t.remote.Codec().ClockRate / 1000)
+ keyframe := true
_, err := t.writer.Write(keyframe, int64(tm), sample.Data)
if err != nil {
return err
@@ -722,67 +626,23 @@ func (t *diskTrack) adjustOrigin(ts uint32) {
}
// called locked
-func (conn *diskConn) initWriter(width, height uint32, track *diskTrack, ts uint32) error {
+func (conn *diskConn) initWriter(track *diskTrack, ts uint32) error {
if conn.file != nil {
- if width == conn.width && height == conn.height {
- return nil
- } else {
- conn.close()
- }
+ conn.close()
}
- isWebm := true
var desc []mkvcore.TrackDescription
for i, t := range conn.tracks {
- var entry webm.TrackEntry
codec := t.remote.Codec()
- if strings.EqualFold(codec.MimeType, "audio/opus") {
- entry = webm.TrackEntry{
- Name: "Audio",
- TrackNumber: uint64(i + 1),
- CodecID: "A_OPUS",
- TrackType: 2,
- Audio: &webm.Audio{
- SamplingFrequency: float64(codec.ClockRate),
- Channels: uint64(codec.Channels),
- },
- }
- } else if strings.EqualFold(codec.MimeType, "video/vp8") {
- entry = webm.TrackEntry{
- Name: "Video",
- TrackNumber: uint64(i + 1),
- CodecID: "V_VP8",
- TrackType: 1,
- Video: &webm.Video{
- PixelWidth: uint64(width),
- PixelHeight: uint64(height),
- },
- }
- } else if strings.EqualFold(codec.MimeType, "video/vp9") {
- entry = webm.TrackEntry{
- Name: "Video",
- TrackNumber: uint64(i + 1),
- CodecID: "V_VP9",
- TrackType: 1,
- Video: &webm.Video{
- PixelWidth: uint64(width),
- PixelHeight: uint64(height),
- },
- }
- } else if strings.EqualFold(codec.MimeType, "video/h264") {
- entry = webm.TrackEntry{
- Name: "Video",
- TrackNumber: uint64(i + 1),
- CodecID: "V_MPEG4/ISO/AVC",
- TrackType: 1,
- Video: &webm.Video{
- PixelWidth: uint64(width),
- PixelHeight: uint64(height),
- },
- }
- isWebm = false
- } else {
- return errors.New("unknown track type")
+ entry := webm.TrackEntry{
+ Name: "Audio",
+ TrackNumber: uint64(i + 1),
+ CodecID: "A_OPUS",
+ TrackType: 2,
+ Audio: &webm.Audio{
+ SamplingFrequency: float64(codec.ClockRate),
+ Channels: uint64(codec.Channels),
+ },
}
desc = append(desc,
mkvcore.TrackDescription{
@@ -792,27 +652,18 @@ func (conn *diskConn) initWriter(width, height uint32, track *diskTrack, ts uint
)
}
- extension := "webm"
- header := webm.DefaultEBMLHeader
- if !isWebm {
- extension = "mkv"
- h := *header
- h.DocType = "matroska"
- header = &h
- }
-
if track != nil {
track.adjustOrigin(ts)
}
- err := conn.open(extension)
+ err := conn.open("webm")
if err != nil {
return err
}
interceptor, err := mkvcore.NewMultiTrackBlockSorter(
// must be larger than the samplebuilder's MaxLate.
- mkvcore.WithMaxDelayedPackets(videoMaxLate+16),
+ mkvcore.WithMaxDelayedPackets(audioMaxLate+16),
mkvcore.WithSortRule(mkvcore.BlockSorterWriteOutdated),
)
if err != nil {
@@ -823,7 +674,7 @@ func (conn *diskConn) initWriter(width, height uint32, track *diskTrack, ts uint
ws, err := mkvcore.NewSimpleBlockWriter(
conn.file, desc,
- mkvcore.WithEBMLHeader(header),
+ mkvcore.WithEBMLHeader(webm.DefaultEBMLHeader),
mkvcore.WithSegmentInfo(webm.DefaultSegmentInfo),
mkvcore.WithBlockInterceptor(interceptor),
)
@@ -839,9 +690,6 @@ func (conn *diskConn) initWriter(width, height uint32, track *diskTrack, ts uint
return errors.New("unexpected number of writers")
}
- conn.width = width
- conn.height = height
-
for i, t := range conn.tracks {
t.writer = ws[i]
}
diff --git a/fork-inventory.md b/fork-inventory.md
new file mode 100644
index 0000000..3d45245
--- /dev/null
+++ b/fork-inventory.md
@@ -0,0 +1,91 @@
+# Skald Fork Inventory
+
+This file records the baseline surfaces found during the early hard-fork
+inventory. It is intentionally short; implementation work should update the
+main docs and tests, not expand this into a second task list.
+
+## Static And Generated Assets
+
+The served static tree is `static/`. Current first-party static files are:
+
+- `404.css`, `404.html`
+- `change-password.css`, `change-password.html`, `change-password.js`
+- `common.css`
+- `index.html`
+- `mainpage.css`, `mainpage.js`
+- `management.js`
+- `protocol.js`
+- `skald.css`, `skald.html`, `skald.js`
+- `stats.html`, `stats.js`
+- `tsconfig.json`
+
+The static tree still includes `static/background-blur-worker.js`, which is a
+video-era asset to remove during the audio-only frontend/backend work.
+
+The `static/example/` directory contains `example.html` and `example.js`.
+
+Third-party static assets are vendored under `static/third-party/`:
+
+- Font Awesome CSS, fonts, and license files.
+- `contextual` CSS/JS and license.
+- `toastify` CSS/JS and license.
+
+No generated static output directory is currently present in the checkout.
+
+## Public URL Surfaces
+
+Current active Skald URL surfaces found in the server and browser code:
+
+- `/hall/name/`
+- `/public-halls.json`
+- `/recordings/`
+- `/skald-api/v0/`
+- `/skald-api/v0/.halls/`
+- `/skald-api/v0/.stats`
+
+Old Galene URL surfaces from the planning checklist are not active routes in
+the current server code:
+
+- `/group/`
+- `/public-groups.json`
+- `/galene-api/`
+- `.groups`
+
+The old `/group/name/` spelling remains in historical `CHANGES` text only.
+
+## Config And Data Paths
+
+Current runtime defaults and config paths:
+
+- `-static ./static/`
+- `-data ./data/`
+- `-halls ./halls/`
+- `-recordings ./recordings/`
+- `data/config.json`
+- `data/ice-servers.json`
+- `data/var/tokens.jsonl`
+- `halls/hallname.json`
+- `skaldctl.json`
+
+The old `groups/` and `galenectl.json` paths are not current runtime defaults.
+Existing Galene installations should be treated as incompatible unless a later
+task explicitly adds a one-shot manual migration tool or documented manual
+conversion procedure.
+
+## Binary, Service, And Install Names
+
+Current binary and service surfaces found in active docs/code:
+
+- Server binary: `skald`
+- Control tool binary: `skaldctl`
+- Service example: `skald.service`
+- Service user and group examples: `skald`
+
+Remaining `galene`, `Galene`, and `galenectl` references are historical
+changelog entries, fork attribution, or the master todo's record of completed
+rename work.
+
+## License And Attribution
+
+The upstream `LICENCE` file remains in place. Skald docs also keep explicit
+fork attribution to Galene where appropriate.
diff --git a/hall/description.go b/hall/description.go
index d87238a..ce9772b 100644
--- a/hall/description.go
+++ b/hall/description.go
@@ -151,7 +151,7 @@ func (u UserDescription) MarshalJSON() ([]byte, error) {
// about the JSON file it was deserialised from.
type Description struct {
// The file this was deserialised from. This is not necessarily
- // the name of the hall, for example in case of a subgroup.
+ // the name of the hall, for example in case of a subhall.
FileName string `json:"-"`
// The modtime and size of the file. These are used to detect
@@ -159,8 +159,8 @@ type Description struct {
modTime time.Time `json:"-"`
fileSize int64 `json:"-"`
- // Whether this is an automatically generated subgroup
- isSubgroup bool `json:"-"`
+ // Whether this is an automatically generated subhall
+ isSubhall bool `json:"-"`
// The user-friendly hall name
DisplayName string `json:"displayName,omitempty"`
@@ -199,8 +199,8 @@ type Description struct {
// Whether creating tokens is allowed
UnrestrictedTokens bool `json:"unrestricted-tokens,omitempty"`
- // Whether subgroups are created on the fly.
- AutoSubgroups bool `json:"auto-subgroups,omitempty"`
+ // Whether subhalls are created on the fly.
+ AutoSubhalls bool `json:"auto-subhalls,omitempty"`
// Whether to lock the hall when the last op logs out.
Autolock bool `json:"autolock,omitempty"`
@@ -231,7 +231,7 @@ type Description struct {
Op []ClientPattern `json:"op,omitempty"`
Presenter []ClientPattern `json:"presenter,omitempty"`
Other []ClientPattern `json:"other,omitempty"`
- AllowSubgroups bool `json:"allow-subgroups,omitempty"`
+ AllowSubhalls bool `json:"allow-subhalls,omitempty"`
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
}
@@ -244,20 +244,20 @@ func maxHistoryAge(desc *Description) time.Duration {
return DefaultMaxHistoryAge
}
-func getDescriptionFile[T any](name string, allowSubgroups bool, get func(string) (T, error)) (T, string, bool, error) {
- isSubgroup := false
+func getDescriptionFile[T any](name string, allowSubhalls bool, get func(string) (T, error)) (T, string, bool, error) {
+ isSubhall := false
for name != "" {
fileName := filepath.Join(
Directory, path.Clean("/"+name)+".json",
)
r, err := get(fileName)
if !errors.Is(err, os.ErrNotExist) {
- return r, fileName, isSubgroup, err
+ return r, fileName, isSubhall, err
}
- if !allowSubgroups {
+ if !allowSubhalls {
break
}
- isSubgroup = true
+ isSubhall = true
name, _ = path.Split(name)
name = strings.TrimRight(name, "/")
}
@@ -311,7 +311,7 @@ func GetSanitisedDescription(name string) (*Description, string, error) {
if err != nil {
return nil, "", err
}
- if d.isSubgroup {
+ if d.isSubhall {
return nil, "", os.ErrNotExist
}
@@ -395,7 +395,7 @@ func rewriteDescriptionFile(filename string, desc *Description) error {
if err != nil {
return err
}
- if !conf.WritableGroups {
+ if !conf.WritableHalls {
return ErrDescriptionsNotWritable
}
@@ -439,9 +439,9 @@ func rewriteDescriptionFile(filename string, desc *Description) error {
}
// readDescription reads a hall's description from disk
-func readDescription(name string, allowSubgroups bool) (*Description, error) {
- r, fileName, isSubgroup, err :=
- getDescriptionFile(name, allowSubgroups, os.Open)
+func readDescription(name string, allowSubhalls bool) (*Description, error) {
+ r, fileName, isSubhall, err :=
+ getDescriptionFile(name, allowSubhalls, os.Open)
if err != nil {
return nil, err
}
@@ -469,11 +469,11 @@ func readDescription(name string, allowSubgroups bool) (*Description, error) {
return nil, err
}
- if isSubgroup {
- if !desc.AutoSubgroups {
+ if isSubhall {
+ if !desc.AutoSubhalls {
return nil, os.ErrNotExist
}
- desc.isSubgroup = true
+ desc.isSubhall = true
desc.Public = false
desc.Description = ""
}
@@ -490,9 +490,9 @@ func upgradeDescription(desc *Description) error {
desc.AllowAnonymous = false
}
- if desc.AllowSubgroups {
- desc.AutoSubgroups = true
- desc.AllowSubgroups = false
+ if desc.AllowSubhalls {
+ desc.AutoSubhalls = true
+ desc.AllowSubhalls = false
}
upgradePassword := func(pw *Password) Password {
diff --git a/hall/description_test.go b/hall/description_test.go
index 24ec663..e625f75 100644
--- a/hall/description_test.go
+++ b/hall/description_test.go
@@ -59,12 +59,12 @@ func TestEmptyJSON(t *testing.T) {
var descJSON = `
{
"max-history-age": 10,
- "auto-subgroups": true,
+ "auto-subhalls": true,
"users": {
- "jch": {"password": "topsecret", "permissions": "op"},
- "john": {"password": "secret", "permissions": "present"},
- "james": {"password": "secret2", "permissions": "message"},
- "peter": {"password": "secret4"}
+ "user1": {"password": "topsecret", "permissions": "op"},
+ "user2": {"password": "secret", "permissions": "present"},
+ "user3": {"password": "secret2", "permissions": "message"},
+ "user4": {"password": "secret4"}
},
"wildcard-user":
{"permissions": "message", "password": {"type":"wildcard"}}
@@ -95,15 +95,15 @@ func TestDescriptionJSON(t *testing.T) {
var obsoleteJSON = `
{
- "op": [{"username": "jch","password": "topsecret"}],
+ "op": [{"username": "user1","password": "topsecret"}],
"max-history-age": 10,
- "allow-subgroups": true,
+ "allow-subhalls": true,
"presenter": [
- {"username": "john", "password": "secret"}
+ {"username": "user2", "password": "secret"}
],
"other": [
- {"username": "james", "password": "secret2"},
- {"username": "peter", "password": "secret4"},
+ {"username": "user3", "password": "secret2"},
+ {"username": "user4", "password": "secret4"},
{}
]
}`
@@ -124,9 +124,9 @@ func TestUpgradeDescription(t *testing.T) {
t.Fatalf("upgradeDescription: %v", err)
}
- if d1.AutoSubgroups != d2.AutoSubgroups ||
- d1.AllowSubgroups != d2.AllowSubgroups {
- t.Errorf("AllowSubgroups not upgraded correctly")
+ if d1.AutoSubhalls != d2.AutoSubhalls ||
+ d1.AllowSubhalls != d2.AllowSubhalls {
+ t.Errorf("AllowSubhalls not upgraded correctly")
}
if d2.Op != nil || d2.Presenter != nil || d2.Other != nil {
@@ -139,7 +139,7 @@ func TestUpgradeDescription(t *testing.T) {
}
for k, v1 := range d1.Users {
- if k == "peter" {
+ if k == "user4" {
// not representable in the old format
continue
}
@@ -176,13 +176,13 @@ func setupTest(dir, datadir string, writable bool) error {
defer f.Close()
conf := `{}`
if writable {
- conf = `{"writableGroups": true}`
+ conf = `{"writableHalls": true}`
}
_, err = f.WriteString(conf)
return err
}
-func TestNonWritableGroups(t *testing.T) {
+func TestNonWritableHalls(t *testing.T) {
err := setupTest(t.TempDir(), t.TempDir(), false)
if err != nil {
t.Fatalf("setupTest: %v", err)
@@ -200,7 +200,7 @@ func TestNonWritableGroups(t *testing.T) {
}
}
-func TestWritableGroups(t *testing.T) {
+func TestWritableHalls(t *testing.T) {
err := setupTest(t.TempDir(), t.TempDir(), true)
if err != nil {
t.Fatalf("setupTest: %v", err)
@@ -259,7 +259,7 @@ func TestWritableGroups(t *testing.T) {
nil, "Test", err, desc.AllowAnonymous,
)
}
- testUser(t, "jch", false)
+ testUser(t, "user1", false)
testUser(t, "", true)
}
@@ -308,7 +308,7 @@ func testUser(t *testing.T, username string, wildcard bool) {
}
}
-func TestSubGroup(t *testing.T) {
+func TestSubHall(t *testing.T) {
err := setupTest(t.TempDir(), t.TempDir(), true)
if err != nil {
t.Fatalf("setupTest: %v", err)
diff --git a/hall/hall.go b/hall/hall.go
index 9c0f8d0..4a124cc 100644
--- a/hall/hall.go
+++ b/hall/hall.go
@@ -216,39 +216,6 @@ func fmtpValue(fmtp, key string) string {
func CodecPayloadType(codec webrtc.RTPCodecCapability) (webrtc.PayloadType, error) {
switch strings.ToLower(codec.MimeType) {
- case "video/vp8":
- return 96, nil
- case "video/vp9":
- profile := fmtpValue(codec.SDPFmtpLine, "profile-id")
- switch profile {
- case "", "0":
- return 98, nil
- case "2":
- return 100, nil
- default:
- return 0, fmt.Errorf("unknown VP9 profile %v", profile)
-
- }
- case "video/av1":
- return 35, nil
- case "video/h264":
- profile := fmtpValue(codec.SDPFmtpLine, "profile-level-id")
- if profile == "" {
- return 102, nil
- }
- if len(profile) < 4 {
- return 0, errors.New("malforned H.264 profile")
- }
- switch strings.ToLower(profile[:4]) {
- case "4200":
- return 102, nil
- case "42e0":
- return 108, nil
- default:
- return 0, fmt.Errorf(
- "unknown H.264 profile %v", profile,
- )
- }
case "audio/opus":
return 111, nil
case "audio/g722":
@@ -262,89 +229,48 @@ func CodecPayloadType(codec webrtc.RTPCodecCapability) (webrtc.PayloadType, erro
}
}
-// VideoRTCPFeedback are the RTCP feedback types that we expect for video
-// tracks.
-var VideoRTCPFeedback = []webrtc.RTCPFeedback{
- {"goog-remb", ""},
- {"nack", ""},
- {"nack", "pli"},
- {"ccm", "fir"},
-}
-
-// AudioRTCPFeedback is like VideoRTCPFeedback but for audio tracks.
+// AudioRTCPFeedback is the RTCP feedback for audio tracks.
var AudioRTCPFeedback = []webrtc.RTCPFeedback(nil)
func codecsFromName(name string) ([]webrtc.RTPCodecParameters, error) {
var codecs []webrtc.RTPCodecCapability
switch name {
- case "vp8":
- codecs = []webrtc.RTPCodecCapability{
- {
- "video/VP8", 90000, 0,
- "",
- VideoRTCPFeedback,
- },
- }
- case "vp9":
- codecs = []webrtc.RTPCodecCapability{
- {
- "video/VP9", 90000, 0,
- "profile-id=0",
- VideoRTCPFeedback,
- },
- {
- "video/VP9", 90000, 0,
- "profile-id=2",
- VideoRTCPFeedback,
- },
- }
- case "av1":
- codecs = []webrtc.RTPCodecCapability{
- {
- "video/AV1", 90000, 0,
- "",
- VideoRTCPFeedback,
- },
- }
- case "h264":
- codecs = []webrtc.RTPCodecCapability{
- {
- "video/H264", 90000, 0,
- "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
- VideoRTCPFeedback,
- },
- }
case "opus":
codecs = []webrtc.RTPCodecCapability{
{
- "audio/opus", 48000, 2,
- "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
- AudioRTCPFeedback,
+ MimeType: "audio/opus",
+ ClockRate: 48000,
+ Channels: 2,
+ SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
+ RTCPFeedback: AudioRTCPFeedback,
},
}
case "g722":
codecs = []webrtc.RTPCodecCapability{
{
- "audio/G722", 8000, 1,
- "",
- AudioRTCPFeedback,
+ MimeType: "audio/G722",
+ ClockRate: 8000,
+ Channels: 1,
+ RTCPFeedback: AudioRTCPFeedback,
},
}
case "pcmu":
codecs = []webrtc.RTPCodecCapability{
{
- "audio/PCMU", 8000, 1,
- "",
- AudioRTCPFeedback,
+ MimeType: "audio/PCMU",
+ ClockRate: 8000,
+ Channels: 1,
+ RTCPFeedback: AudioRTCPFeedback,
},
}
case "pcma":
codecs = []webrtc.RTPCodecCapability{
{
- "audio/PCMA", 8000, 1,
- "",
- AudioRTCPFeedback,
+ MimeType: "audio/PCMA",
+ ClockRate: 8000,
+ Channels: 1,
+ RTCPFeedback: AudioRTCPFeedback,
},
}
default:
@@ -384,11 +310,7 @@ func APIFromCodecs(codecs []webrtc.RTPCodecParameters) (*webrtc.API, error) {
m := webrtc.MediaEngine{}
for _, codec := range codecs {
- tpe := webrtc.RTPCodecTypeVideo
- if strings.HasPrefix(strings.ToLower(codec.MimeType), "audio/") {
- tpe = webrtc.RTPCodecTypeAudio
- }
- err := m.RegisterCodec(codec, tpe)
+ err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio)
if err != nil {
log.Printf("%v", err)
continue
@@ -401,11 +323,6 @@ func APIFromCodecs(codecs []webrtc.RTPCodecParameters) (*webrtc.API, error) {
s.SetEphemeralUDPPortRange(UDPMin, UDPMax)
}
- err := webrtc.ConfigureSimulcastExtensionHeaders(&m)
- if err != nil {
- return nil, err
- }
-
ir := interceptor.Registry{}
return webrtc.NewAPI(
@@ -417,7 +334,7 @@ func APIFromCodecs(codecs []webrtc.RTPCodecParameters) (*webrtc.API, error) {
func APIFromNames(names []string) (*webrtc.API, error) {
if len(names) == 0 {
- names = []string{"vp8", "opus"}
+ names = []string{"opus"}
}
var codecs []webrtc.RTPCodecParameters
for _, n := range names {
@@ -543,14 +460,14 @@ func GetNames() []string {
return names
}
-type SubGroup struct {
+type SubHall struct {
Name string
Clients int
}
-func GetSubGroups(parent string) []SubGroup {
+func GetSubHalls(parent string) []SubHall {
prefix := parent + "/"
- subgroups := make([]SubGroup, 0)
+ subhalls := make([]SubHall, 0)
Range(func(g *Hall) bool {
if strings.HasPrefix(g.name, prefix) {
@@ -558,13 +475,13 @@ func GetSubGroups(parent string) []SubGroup {
count := len(g.clients)
g.mu.Unlock()
if count > 0 {
- subgroups = append(subgroups,
- SubGroup{g.name, count})
+ subhalls = append(subhalls,
+ SubHall{g.name, count})
}
}
return true
})
- return subgroups
+ return subhalls
}
func Get(name string) *Hall {
@@ -929,7 +846,7 @@ type Configuration struct {
AllowOrigin []string `json:"allowOrigin,omitempty"`
AllowAdminOrigin []string `json:"allowAdminOrigin,omitempty"`
ProxyURL string `json:"proxyURL,omitempty"`
- WritableGroups bool `json:"writableGroups,omitempty"`
+ WritableHalls bool `json:"writableHalls,omitempty"`
Users map[string]UserDescription `json:"users,omitempty"`
// obsolete fields
@@ -1173,7 +1090,7 @@ func (g *Hall) Status(authentified bool, base *url.URL) Status {
if authentified {
conf, err := GetConfiguration()
if err == nil {
- d.CanChangePassword = conf.WritableGroups
+ d.CanChangePassword = conf.WritableHalls
}
}
return d
diff --git a/hall/hall_test.go b/hall/hall_test.go
index 59ffd45..041df3a 100644
--- a/hall/hall_test.go
+++ b/hall/hall_test.go
@@ -34,23 +34,23 @@ func TestConstantTimeCompare(t *testing.T) {
}
}
-func TestGroup(t *testing.T) {
+func TestHall(t *testing.T) {
halls.halls = nil
Add("hall", &Description{})
- Add("hall/subgroup", &Description{Public: true})
+ Add("hall/subhall", &Description{Public: true})
if len(halls.halls) != 2 {
t.Errorf("Expected 2, got %v", len(halls.halls))
}
g := Get("hall")
- g2 := Get("hall/subgroup")
+ g2 := Get("hall/subhall")
if g == nil {
t.Fatalf("Couldn't get hall")
}
if g2 == nil {
- t.Fatalf("Couldn't get hall/subgroup")
+ t.Fatalf("Couldn't get hall/subhall")
}
if name := g.Name(); name != "hall" {
- t.Errorf("Name: expected group1, got %v", name)
+ t.Errorf("Name: expected hall, got %v", name)
}
if locked, _ := g.Locked(); locked {
t.Errorf("Locked: expected false, got %v", locked)
@@ -64,12 +64,12 @@ func TestGroup(t *testing.T) {
t.Errorf("Expected 2, got %v", names)
}
- if subs := GetSubGroups("hall"); len(subs) != 0 {
+ if subs := GetSubHalls("hall"); len(subs) != 0 {
t.Errorf("Expected [], got %v", subs)
}
- if public := GetPublic(nil); len(public) != 1 || public[0].Name != "hall/subgroup" {
- t.Errorf("Expected hall/subgroup, got %v", public)
+ if public := GetPublic(nil); len(public) != 1 || public[0].Name != "hall/subhall" {
+ t.Errorf("Expected hall/subhall, got %v", public)
}
}
@@ -167,7 +167,7 @@ type credPerm struct {
var desc2JSON = `
{
"max-history-age": 10,
- "auto-subgroups": true,
+ "auto-subhalls": true,
"users": {
"jch": {"password": "topsecret", "permissions": "op"},
"john": {"password": "secret", "permissions": "present"},
@@ -377,7 +377,6 @@ func TestValidHallName(t *testing.T) {
func TestPayloadTypeDistinct(t *testing.T) {
names := []string{
- "vp8", "vp9", "av1", "h264",
"opus", "g722", "pcmu", "pcma",
}
diff --git a/packetcache/packetcache_test.go b/packetcache/packetcache_test.go
index 5dbc035..f5ee203 100644
--- a/packetcache/packetcache_test.go
+++ b/packetcache/packetcache_test.go
@@ -270,7 +270,10 @@ func TestBitmapPacket(t *testing.T) {
t.Fatalf("Didn't find any 0 bits")
}
- p := rtcp.NackPair{first, rtcp.PacketBitmap(bitmap)}
+ p := rtcp.NackPair{
+ PacketID: first,
+ LostPackets: rtcp.PacketBitmap(bitmap),
+ }
p.Range(func(s uint16) bool {
if s < 42 || s >= 42+64 {
if (value & (1 << (s - 42))) != 0 {
@@ -403,7 +406,10 @@ func TestToBitmapNack(t *testing.T) {
for len(m) > 0 {
var f, b uint16
f, b, m = ToBitmap(m)
- nacks = append(nacks, rtcp.NackPair{f, rtcp.PacketBitmap(b)})
+ nacks = append(nacks, rtcp.NackPair{
+ PacketID: f,
+ LostPackets: rtcp.PacketBitmap(b),
+ })
}
var n []uint16
for len(nacks) > 0 {
diff --git a/rtpconn/rtpconn.go b/rtpconn/rtpconn.go
index 54db4ee..92ea956 100644
--- a/rtpconn/rtpconn.go
+++ b/rtpconn/rtpconn.go
@@ -230,68 +230,12 @@ func (down *rtpDownTrack) Write(buf []byte) (int, error) {
return 0, err
}
- layer := down.getLayerInfo()
-
- if flags.Tid > layer.maxTid || flags.Sid > layer.maxSid {
- if flags.Tid > layer.maxTid {
- // increase eagerly if this is the first time we
- // see a given layer
- if layer.tid == layer.maxTid {
- layer.wantedTid = flags.Tid
- layer.tid = flags.Tid
- }
- layer.maxTid = flags.Tid
- }
- if flags.Sid > layer.maxSid {
- if layer.sid == layer.maxSid && !layer.limitSid {
- layer.wantedSid = flags.Sid
- layer.sid = flags.Sid
- }
- layer.maxSid = flags.Sid
- }
- down.setLayerInfo(layer)
- down.adjustLayer()
- layer = down.getLayerInfo()
- }
-
- if flags.Start && (layer.tid != layer.wantedTid) {
- if flags.Keyframe {
- layer.tid = layer.wantedTid
- down.setLayerInfo(layer)
- } else if layer.wantedTid < layer.tid {
- layer.tid = layer.wantedTid
- down.setLayerInfo(layer)
- } else if flags.TidUpSync && flags.Tid <= layer.wantedTid {
- layer.tid = flags.Tid
- down.setLayerInfo(layer)
- }
- }
-
- if flags.Start && (layer.sid != layer.wantedSid) {
- if flags.Keyframe {
- layer.sid = layer.wantedSid
- down.setLayerInfo(layer)
- } else {
- down.remote.RequestKeyframe()
- }
- }
-
- if flags.Tid > layer.tid || flags.Sid > layer.sid ||
- (flags.Sid < layer.sid && flags.SidNonReference) {
- ok := down.packetmap.Drop(flags.Seqno, flags.Pid)
- if ok {
- return 0, nil
- }
- }
-
ok, newseqno, piddelta := down.packetmap.Map(flags.Seqno, flags.Pid)
if !ok {
return 0, nil
}
- setMarker := flags.Sid == layer.sid && flags.End && !flags.Marker
-
- if !setMarker && newseqno == flags.Seqno && piddelta == 0 {
+ if newseqno == flags.Seqno && piddelta == 0 {
return down.write(buf)
}
@@ -300,7 +244,7 @@ func (down *rtpDownTrack) Write(buf []byte) (int, error) {
buf2 := ibuf2.([]byte)
n := copy(buf2, buf)
- err = codecs.RewritePacket(codec, buf2[:n], setMarker, newseqno, piddelta)
+ err = codecs.RewritePacket(codec, buf2[:n], false, newseqno, piddelta)
if err != nil {
return 0, err
}
@@ -420,7 +364,6 @@ type trackActionKind int
const (
trackActionAdd trackActionKind = iota
trackActionDel
- trackActionKeyframe
)
type trackAction struct {
@@ -454,11 +397,6 @@ func (up *rtpUpTrack) AddLocal(local conn.DownTrack) error {
return nil
}
-func (up *rtpUpTrack) RequestKeyframe() error {
- up.action(trackActionKeyframe, nil)
- return nil
-}
-
func (up *rtpUpTrack) DelLocal(local conn.DownTrack) bool {
up.mu.Lock()
for i, l := range up.local {
@@ -598,7 +536,7 @@ func (up *rtpUpConnection) flushICECandidates() error {
return err
}
-// pushConnNow pushes a connection to all of the clients in a group
+// pushConnNow pushes a connection to all of the clients in a hall
func pushConnNow(up *rtpUpConnection, g *hall.Hall, cs []hall.Client) {
up.mu.Lock()
up.pushed = true
@@ -650,8 +588,13 @@ func newUpConn(c hall.Client, id string, label string, offer string) (*rtpUpConn
}
for _, m := range o.MediaDescriptions {
+ tpe := webrtc.NewRTPCodecType(m.MediaName.Media)
+ if tpe == webrtc.RTPCodecTypeVideo {
+ log.Printf("Rejecting incoming video media section")
+ continue
+ }
_, err = pc.AddTransceiverFromKind(
- webrtc.NewRTPCodecType(m.MediaName.Media),
+ tpe,
webrtc.RTPTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionRecvonly,
},
@@ -665,6 +608,10 @@ func newUpConn(c hall.Client, id string, label string, offer string) (*rtpUpConn
up := &rtpUpConnection{id: id, client: c, label: label, pc: pc}
pc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
+ if remote.Kind() == webrtc.RTPCodecTypeVideo {
+ log.Printf("Rejecting incoming video track")
+ return
+ }
up.mu.Lock()
track := &rtpUpTrack{
@@ -698,26 +645,16 @@ func newUpConn(c hall.Client, id string, label string, offer string) (*rtpUpConn
var ErrUnsupportedFeedback = errors.New("unsupported feedback type")
var ErrRateLimited = errors.New("rate limited")
-func (track *rtpUpTrack) sendPLI() error {
- if !track.hasRtcpFb("nack", "pli") {
- return ErrUnsupportedFeedback
- }
- return sendPLI(track.conn.pc, track.track.SSRC())
-}
-
-func sendPLI(pc *webrtc.PeerConnection, ssrc webrtc.SSRC) error {
- return pc.WriteRTCP([]rtcp.Packet{
- &rtcp.PictureLossIndication{MediaSSRC: uint32(ssrc)},
- })
-}
-
func (track *rtpUpTrack) sendNACK(first uint16, bitmap uint16) error {
if !track.hasRtcpFb("nack", "") {
return ErrUnsupportedFeedback
}
err := sendNACKs(track.conn.pc, track.track.SSRC(),
- []rtcp.NackPair{{first, rtcp.PacketBitmap(bitmap)}},
+ []rtcp.NackPair{{
+ PacketID: first,
+ LostPackets: rtcp.PacketBitmap(bitmap),
+ }},
)
if err == nil {
track.cache.Expect(1 + bits.OnesCount16(bitmap))
@@ -744,7 +681,10 @@ func (track *rtpUpTrack) sendNACKs(seqnos []uint16) error {
}
var f, b uint16
f, b, seqnos = packetcache.ToBitmap(seqnos)
- nacks = append(nacks, rtcp.NackPair{f, rtcp.PacketBitmap(b)})
+ nacks = append(nacks, rtcp.NackPair{
+ PacketID: f,
+ LostPackets: rtcp.PacketBitmap(b),
+ })
}
err := sendNACKs(track.conn.pc, track.track.SSRC(), nacks)
if err == nil {
@@ -814,7 +754,7 @@ func rtcpUpListener(track *rtpUpTrack) {
for {
firstSR := false
- n, _, err := track.receiver.ReadSimulcast(buf, track.track.RID())
+ n, _, err := track.receiver.Read(buf)
if err != nil {
if err != io.EOF && err != io.ErrClosedPipe {
log.Printf("Read RTCP: %v", err)
@@ -1147,7 +1087,6 @@ func (track *rtpDownTrack) updateRate(loss uint8, now uint64) {
}
func rtcpDownListener(track *rtpDownTrack) {
- lastFirSeqno := uint8(0)
buf := make([]byte, 1500)
@@ -1165,40 +1104,18 @@ func rtcpDownListener(track *rtpDownTrack) {
continue
}
- adjust := false
jiffies := rtptime.Jiffies()
for _, p := range ps {
switch p := p.(type) {
- case *rtcp.PictureLossIndication:
- track.remote.RequestKeyframe()
- case *rtcp.FullIntraRequest:
- found := false
- var seqno uint8
- for _, entry := range p.FIR {
- if entry.SSRC == uint32(track.ssrc) {
- found = true
- seqno = entry.SequenceNumber
- break
- }
- }
- if !found {
- log.Printf("Misdirected FIR")
- continue
- }
- if seqno != lastFirSeqno {
- track.remote.RequestKeyframe()
- }
case *rtcp.ReceiverEstimatedMaximumBitrate:
rate := uint64(p.Bitrate + 0.5)
track.maxREMBBitrate.Set(rate, jiffies)
- adjust = true
case *rtcp.ReceiverReport:
for _, r := range p.Reports {
if r.SSRC == uint32(track.ssrc) {
handleReport(track, r, jiffies)
- adjust = true
}
}
case *rtcp.SenderReport:
@@ -1211,9 +1128,7 @@ func rtcpDownListener(track *rtpDownTrack) {
gotNACK(track, p)
}
}
- if adjust {
- track.adjustLayer()
- }
+
}
}
@@ -1245,9 +1160,6 @@ func handleReport(track *rtpDownTrack, report rtcp.ReceptionReport, jiffies uint
}
func minPacketCache(track *webrtc.TrackRemote) int {
- if track.Kind() == webrtc.RTPCodecTypeVideo {
- return 128
- }
return 24
}
diff --git a/rtpconn/rtpreader.go b/rtpconn/rtpreader.go
index ecf14ac..2dd3f37 100644
--- a/rtpconn/rtpreader.go
+++ b/rtpconn/rtpreader.go
@@ -3,12 +3,9 @@ package rtpconn
import (
"io"
"log"
- "time"
"github.com/pion/rtp"
- "github.com/pion/webrtc/v4"
- "git.stormux.org/storm/skald/codecs"
"git.stormux.org/storm/skald/packetcache"
"git.stormux.org/storm/skald/rtptime"
)
@@ -20,12 +17,7 @@ func readLoop(track *rtpUpTrack) {
close(track.readerDone)
}()
- isvideo := track.track.Kind() == webrtc.RTPCodecTypeVideo
- codec := track.track.Codec()
sendNACK := track.hasRtcpFb("nack", "")
- sendPLI := track.hasRtcpFb("nack", "pli")
- var kfNeeded bool
- var kfRequested time.Time
buf := make([]byte, packetcache.BufSize)
var packet rtp.Packet
for {
@@ -46,8 +38,6 @@ func readLoop(track *rtpUpTrack) {
err,
)
}
- case trackActionKeyframe:
- kfNeeded = true
default:
log.Printf("Unknown action")
}
@@ -72,10 +62,6 @@ func readLoop(track *rtpUpTrack) {
track.jitter.Accumulate(packet.Timestamp)
- kf, kfKnown := codecs.Keyframe(codec.MimeType, &packet)
- if kf || !kfKnown {
- kfNeeded = false
- }
if packet.Extension {
packet.Extension = false
packet.Extensions = nil
@@ -88,7 +74,7 @@ func readLoop(track *rtpUpTrack) {
first, index := track.cache.Store(
packet.SequenceNumber, packet.Timestamp,
- kf, packet.Marker, buf[:bytes],
+ false, packet.Marker, buf[:bytes],
)
_, rate := track.rate.Estimate()
@@ -131,20 +117,6 @@ func readLoop(track *rtpUpTrack) {
}
writers.write(packet.SequenceNumber, index, delay,
- isvideo, packet.Marker)
-
- now := time.Now()
- if kfNeeded && now.Sub(kfRequested) > time.Second/2 {
- if sendPLI {
- err := track.sendPLI()
- if err != nil {
- log.Printf("sendPLI: %v", err)
- kfNeeded = false
- }
- } else {
- kfNeeded = false
- }
- kfRequested = now
- }
+ packet.Marker)
}
}
diff --git a/rtpconn/rtpwriter.go b/rtpconn/rtpwriter.go
index 7f4f524..56484c9 100644
--- a/rtpconn/rtpwriter.go
+++ b/rtpconn/rtpwriter.go
@@ -105,7 +105,7 @@ func (wp *rtpWriterPool) close() {
}
// write writes a packet stored in the packet cache to all local tracks
-func (wp *rtpWriterPool) write(seqno uint16, index uint16, delay uint32, isvideo bool, marker bool) {
+func (wp *rtpWriterPool) write(seqno uint16, index uint16, delay uint32, marker bool) {
pi := packetIndex{seqno, index}
var dead []*rtpWriter
@@ -127,15 +127,7 @@ func (wp *rtpWriterPool) write(seqno uint16, index uint16, delay uint32, isvideo
// the writer is dead.
dead = append(dead, w)
default:
- // the writer is congested
- if isvideo {
- // drop until the end of the frame
- if !marker {
- w.drop = 7
- }
- continue
- }
- // audio, try again with a delay
+ // the writer is congested, try again with a delay
d := delay / uint32(2*len(wp.writers))
timer := time.NewTimer(rtptime.ToDuration(
int64(d), rtptime.JiffiesPerSec,
@@ -256,20 +248,12 @@ func rtpWriterLoop(writer *rtpWriter, track *rtpUpTrack) {
}
last, foundLast := track.cache.Last()
- kf, foundKf := track.cache.Keyframe()
- if foundLast && foundKf {
- if last-kf < 40 { // modulo 2^16
- go sendSequence(
- kf, last,
- action.track,
- track.cache,
- )
- } else {
- track.RequestKeyframe()
- }
- } else {
- // no keyframe yet, one should
- // arrive soon. Do nothing.
+ if foundLast {
+ go sendSequence(
+ last, last,
+ action.track,
+ track.cache,
+ )
}
} else {
found := false
diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go
index 377164d..9e026c1 100644
--- a/rtpconn/webclient.go
+++ b/rtpconn/webclient.go
@@ -10,7 +10,6 @@ import (
"maps"
"net"
"os"
- "strings"
"sync"
"time"
@@ -109,9 +108,9 @@ func (c *webClient) SetPermissions(perms []string) {
c.permissions = perms
}
-func (c *webClient) PushClient(group, kind, id string, username string, perms []string, data map[string]interface{}) error {
+func (c *webClient) PushClient(hall, kind, id string, username string, perms []string, data map[string]interface{}) error {
c.action(pushClientAction{
- group, kind, id, username, perms, data,
+ hall, kind, id, username, perms, data,
})
return nil
}
@@ -132,7 +131,7 @@ type clientMessage struct {
Permissions []string `json:"permissions,omitempty"`
Status *hall.Status `json:"status,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
- Group string `json:"group,omitempty"`
+ Hall string `json:"hall,omitempty"`
Value interface{} `json:"value,omitempty"`
NoEcho bool `json:"noecho,omitempty"`
Time string `json:"time,omitempty"`
@@ -385,11 +384,7 @@ func addDownTrackUnlocked(conn *rtpDownConnection, remoteTrack *rtpUpTrack) erro
// replace the RTCP feedback types with the ones we understand
remoteCodec := remoteTrack.Codec()
- if strings.HasPrefix(strings.ToLower(remoteCodec.MimeType), "video/") {
- remoteCodec.RTCPFeedback = hall.VideoRTCPFeedback
- } else {
- remoteCodec.RTCPFeedback = hall.AudioRTCPFeedback
- }
+ remoteCodec.RTCPFeedback = hall.AudioRTCPFeedback
local, err := webrtc.NewTrackLocalStaticRTP(
remoteCodec, id, msid,
@@ -727,7 +722,7 @@ func parseRequested(r interface{}) (map[string][]string, error) {
func (c *webClient) setRequested(requested map[string][]string) error {
if c.hall == nil {
- return errors.New("attempted to request with no group joined")
+ return errors.New("attempted to request with no hall joined")
}
c.requested = requested
@@ -761,60 +756,27 @@ func requestedTracks(c *webClient, requested []string, tracks []conn.UpTrack) ([
if len(requested) == 0 {
return nil, false
}
- var audio, video, videoLow bool
+ var audio bool
for _, s := range requested {
switch s {
case "audio":
audio = true
- case "video":
- video = true
- case "video-low":
- videoLow = true
default:
log.Printf("client requested unknown value %v", s)
}
}
- find := func(kind webrtc.RTPCodecType, last bool) (conn.UpTrack, int) {
- var track conn.UpTrack
- count := 0
+ var ts []conn.UpTrack
+ if audio {
for _, t := range tracks {
- if t.Kind() != kind {
- continue
- }
- track = t
- count++
- if !last {
+ if t.Kind() == webrtc.RTPCodecTypeAudio {
+ ts = append(ts, t)
break
}
}
- return track, count
}
- var ts []conn.UpTrack
- limitSid := false
- if audio {
- t, _ := find(webrtc.RTPCodecTypeAudio, false)
- if t != nil {
- ts = append(ts, t)
- }
- }
- if video {
- t, _ := find(webrtc.RTPCodecTypeVideo, false)
- if t != nil {
- ts = append(ts, t)
- }
- } else if videoLow {
- t, count := find(webrtc.RTPCodecTypeVideo, true)
- if t != nil {
- ts = append(ts, t)
- }
- if count < 2 {
- limitSid = true
- }
- }
-
- return ts, limitSid
+ return ts, false
}
func (c *webClient) PushConn(g *hall.Hall, id string, up conn.Up, tracks []conn.UpTrack, replace string) error {
@@ -914,7 +876,7 @@ type connectionFailedAction struct {
}
type pushClientAction struct {
- group string
+ hall string
kind string
id string
username string
@@ -929,8 +891,8 @@ type changePermissionsAction struct {
type permissionsChangedAction struct{}
type joinedAction struct {
- group string
- kind string
+ hall string
+ kind string
}
type kickAction struct {
@@ -972,7 +934,7 @@ func clientLoop(c *webClient, ws *websocket.Conn, versionError bool) error {
read := make(chan interface{}, 1)
go clientReader(ws, read, c.done)
- defer leaveGroup(c)
+ defer leaveHall(c)
readTime := time.Now()
@@ -1109,7 +1071,7 @@ func handleAction(c *webClient, a any) error {
switch a := a.(type) {
case pushConnAction:
if c.hall == nil || c.hall != a.hall {
- log.Printf("Got connectsions for wrong group")
+ log.Printf("Got connectsions for wrong hall")
return nil
}
return pushDownConn(c, a.id, a.conn, a.tracks, a.replace)
@@ -1163,7 +1125,7 @@ func handleAction(c *webClient, a any) error {
}
case pushClientAction:
- if a.group != c.hall.Name() {
+ if a.hall != c.hall.Name() {
log.Printf("got client for wrong hall")
return nil
}
@@ -1181,8 +1143,8 @@ func handleAction(c *webClient, a any) error {
var status *hall.Status
var data map[string]interface{}
var g *hall.Hall
- if a.group != "" {
- g = hall.Get(a.group)
+ if a.hall != "" {
+ g = hall.Get(a.hall)
if g != nil {
s := g.Status(true, nil)
status = &s
@@ -1194,7 +1156,7 @@ func handleAction(c *webClient, a any) error {
err := c.write(clientMessage{
Type: "joined",
Kind: a.kind,
- Group: a.group,
+ Hall: a.hall,
Username: &username,
Permissions: perms,
Status: status,
@@ -1252,7 +1214,7 @@ func handleAction(c *webClient, a any) error {
case permissionsChangedAction:
g := c.Hall()
if g == nil {
- return errors.New("Permissions changed in no group")
+ return errors.New("Permissions changed in no hall")
}
perms := append([]string(nil), c.permissions...)
status := g.Status(true, nil)
@@ -1260,7 +1222,7 @@ func handleAction(c *webClient, a any) error {
c.write(clientMessage{
Type: "joined",
Kind: "change",
- Group: g.Name(),
+ Hall: g.Name(),
Username: &username,
Permissions: perms,
Status: &status,
@@ -1293,7 +1255,9 @@ func handleAction(c *webClient, a any) error {
}(clients)
case kickAction:
return hall.KickError{
- a.id, a.username, a.message,
+ Id: a.id,
+ Username: a.username,
+ Message: a.message,
}
default:
log.Printf("unexpected action %T", a)
@@ -1321,7 +1285,7 @@ func failUpConnection(c *webClient, id string, message string) error {
return nil
}
-func leaveGroup(c *webClient) {
+func leaveHall(c *webClient) {
if c.hall == nil {
return
}
@@ -1370,8 +1334,8 @@ func (c *webClient) Kick(id string, user *string, message string) error {
return nil
}
-func (c *webClient) Joined(group, kind string) error {
- c.action(joinedAction{group, kind})
+func (c *webClient) Joined(hall, kind string) error {
+ c.action(joinedAction{hall, kind})
return nil
}
@@ -1402,10 +1366,10 @@ func handleClientMessage(c *webClient, m clientMessage) error {
switch m.Type {
case "join":
if m.Kind == "leave" {
- if c.hall == nil || c.hall.Name() != m.Group {
+ if c.hall == nil || c.hall.Name() != m.Hall {
return hall.UserError("you are not joined")
}
- leaveGroup(c)
+ leaveHall(c)
return nil
}
@@ -1415,11 +1379,11 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if c.hall != nil {
return hall.ProtocolError(
- "cannot join multiple groups",
+ "cannot join multiple halls",
)
}
c.data = m.Data
- g, err := hall.AddClient(m.Group, c,
+ g, err := hall.AddClient(m.Hall, c,
hall.ClientCredentials{
Username: m.Username,
Password: m.Password,
@@ -1438,33 +1402,33 @@ func handleClientMessage(c *webClient, m clientMessage) error {
} else if errors.As(err, &autherr) {
s = "not authorised"
time.Sleep(200 * time.Millisecond)
- log.Printf("Join group: %v", err)
+ log.Printf("Join hall: %v", err)
} else if errors.Is(err, os.ErrNotExist) {
- s = "group does not exist"
+ s = "hall does not exist"
} else if _, ok := err.(hall.UserError); ok {
s = err.Error()
} else {
s = "internal server error"
- log.Printf("Join group: %v", err)
+ log.Printf("Join hall: %v", err)
}
username := c.username
return c.write(clientMessage{
Type: "joined",
Kind: "fail",
Error: e,
- Group: m.Group,
+ Hall: m.Hall,
Username: &username,
Value: s,
})
}
if redirect := g.Description().Redirect; redirect != "" {
- // We normally redirect at the HTTP level, but the group
+ // We normally redirect at the HTTP level, but the hall
// description could have been edited in the meantime.
username := c.username
return c.write(clientMessage{
Type: "joined",
Kind: "redirect",
- Group: m.Group,
+ Hall: m.Hall,
Username: &username,
Value: redirect,
})
@@ -1574,7 +1538,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
case "chat", "usermessage":
g := c.hall
if g == nil {
- return c.error(hall.UserError("join a group first"))
+ return c.error(hall.UserError("join a hall first"))
}
required := "message"
@@ -1635,10 +1599,10 @@ func handleClientMessage(c *webClient, m clientMessage) error {
}
ccc.write(mm)
}
- case "groupaction":
+ case "hallaction":
g := c.hall
if g == nil {
- return c.error(hall.UserError("join a group first"))
+ return c.error(hall.UserError("join a hall first"))
}
switch m.Kind {
case "clearchat":
@@ -1714,12 +1678,12 @@ func handleClientMessage(c *webClient, m clientMessage) error {
hall.DelClient(disk)
}
}
- case "subgroups":
+ case "subhalls":
if !member("op", c.permissions) {
return c.error(hall.UserError("not authorised"))
}
s := ""
- for _, sg := range hall.GetSubGroups(g.Name()) {
+ for _, sg := range hall.GetSubHalls(g.Name()) {
plural := ""
if sg.Clients > 1 {
plural = "s"
@@ -1773,10 +1737,10 @@ func handleClientMessage(c *webClient, m clientMessage) error {
return terror("error", "client specified token")
}
- if tok.Group != c.hall.Name() {
- return terror("error", "wrong group in token")
+ if tok.Hall != c.hall.Name() {
+ return terror("error", "wrong hall in token")
}
- if tok.IncludeSubgroups {
+ if tok.IncludeSubhalls {
return terror("error",
"hierarchical token not allowed",
)
@@ -1835,8 +1799,8 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if err != nil {
return terror("error", err.Error())
}
- if tok.Group != "" || tok.Username != nil ||
- tok.IncludeSubgroups ||
+ if tok.Hall != "" || tok.Username != nil ||
+ tok.IncludeSubhalls ||
tok.Permissions != nil ||
tok.IssuedBy != nil ||
tok.IssuedAt != nil {
@@ -1892,12 +1856,12 @@ func handleClientMessage(c *webClient, m clientMessage) error {
Value: tokens,
})
default:
- return hall.UserError("unknown group action")
+ return hall.UserError("unknown hall action")
}
case "useraction":
g := c.hall
if g == nil {
- return c.error(hall.UserError("join a group first"))
+ return c.error(hall.UserError("join a hall first"))
}
switch m.Kind {
case "op", "unop", "present", "unpresent", "shutup", "unshutup":
@@ -2076,7 +2040,7 @@ func parseStatefulToken(value interface{}) (*token.Stateful, error) {
if err != nil {
return nil, err
}
- g, err := parseString("group")
+ g, err := parseString("hall")
if err != nil {
return nil, err
}
@@ -2098,7 +2062,7 @@ func parseStatefulToken(value interface{}) (*token.Stateful, error) {
}
return &token.Stateful{
Token: tt,
- Group: gg,
+ Hall: gg,
Username: u,
Permissions: p,
Expires: e,
diff --git a/rtpconn/webclient_test.go b/rtpconn/webclient_test.go
index 7939579..12b10d6 100644
--- a/rtpconn/webclient_test.go
+++ b/rtpconn/webclient_test.go
@@ -11,18 +11,18 @@ import (
var tokens = []string{
`{
"token": "a",
- "group": "g",
+ "hall": "h",
"username": "u",
"permissions":["present"],
"expires": "2023-05-03T20:24:47.616624532+02:00"
}`,
`{
"token": "a",
- "group": "g"
+ "hall": "h"
}`,
`{
"token": "a",
- "group": "g",
+ "hall": "h",
"username":""
}`,
}
diff --git a/rtpconn/whipclient.go b/rtpconn/whipclient.go
index 9c3d84a..46ea2a1 100644
--- a/rtpconn/whipclient.go
+++ b/rtpconn/whipclient.go
@@ -107,11 +107,11 @@ func (c *WhipClient) RequestConns(target hall.Client, g *hall.Hall, id string) e
return nil
}
-func (c *WhipClient) Joined(group, kind string) error {
+func (c *WhipClient) Joined(hall, kind string) error {
return nil
}
-func (c *WhipClient) PushClient(group, kind, id, username string, permissions []string, status map[string]interface{}) error {
+func (c *WhipClient) PushClient(hall, kind, id, username string, permissions []string, status map[string]interface{}) error {
return nil
}
diff --git a/skald-api.md b/skald-api.md
index 41d4825..eefbb69 100644
--- a/skald-api.md
+++ b/skald-api.md
@@ -7,19 +7,19 @@ users. For example, in order to create a hall, a client may do
Content-Type: application/json
If-None-Match: *
-The `If-None-Match` header avoids overwriting an existing group.
+The `If-None-Match` header avoids overwriting an existing hall.
In order to edit a hall definition, a client first does
GET /skald-api/v0/.halls/hallname/
This yields the hall definition and an entity tag (in the ETag header).
-The client then modifies the group defintion, and does
+The client then modifies the hall definition, and does
PUT /skald-api/v0/.halls/hallname/
If-Match: "abcd"
-where "abcd" is the entity tag returned by the GET request. If the group
+where "abcd" is the entity tag returned by the GET request. If the hall
definition has changed in the meantime, the entity tag will no longer be
valid, and the server will fail the update, which avoids losing an update
in the case of a concurrent modification.
@@ -46,7 +46,7 @@ allowed methods are HEAD and GET.
Returns a list of halls, as a JSON array. The only allowed methods are
HEAD and GET.
-### Group definition
+### Hall definition
/skald-api/v0/.halls/hallname
diff --git a/skald-client.md b/skald-client.md
index d2f33bc..d284321 100644
--- a/skald-client.md
+++ b/skald-client.md
@@ -19,15 +19,14 @@ well as all the associated streams. Unless your frontend communicates
with multiple servers, it will probably create just a single instance of
this class.
-The class `Stream` encapsulates a set of related audio and video tracks
-(for example, an audio track from a microphone and a video track from
-a webcam). A stream is said to go *up* when it carries data from the
-client to the server, and *down* otherwise. Streams going up are created
-by the client (your frontend), streams going down are created by the server.
+The class `Stream` encapsulates a set of related audio tracks. A stream
+is said to go *up* when it carries data from the client to the server, and
+*down* otherwise. Streams going up are created by the client (your
+frontend), streams going down are created by the server.
## Connecting to the server
-First, fetch the `.status` JSON at the group URL:
+First, fetch the `.status` JSON at the hall URL:
```javascript
let r = await fetch(url + ".status");
@@ -57,11 +56,11 @@ have been closed by the time it is called. The `onusermessage` callback
indicates an application-specific message, either from another user or
from the server; the field `kind` indicates the kind of message.
-Once you have joined a group (see below), the remaining callbacks may
+Once you have joined a hall (see below), the remaining callbacks may
trigger. The `onuser` callback is used to indicate that a user has joined
-or left the current group, or that their attributes have changed; the
+or left the current hall, or that their attributes have changed; the
user's state can be found in the `users` dictionary. The `onchat`
-callback indicates that a chat message has been posted to the group, and
+callback indicates that a chat message has been posted to the hall, and
`onclearchat` indicates that the chat history has been cleared. Finally,
`ondownstream` is called when the server pushes a stream to the client;
see the section below about streams.
@@ -72,11 +71,11 @@ You may now connect to the server:
serverConnection.connect(status.endpoint);
```
-You typically join a group in the `onconnected` callback:
+You typically join a hall in the `onconnected` callback:
```javascript
serverConnection.onconnected = function() {
- this.join(group, 'join', username, password);
+ this.join(hall, 'join', username, password);
}
```
@@ -85,10 +84,10 @@ will trigger. There, you update your user interface and request incoming
streams:
```javascript
-serverConnection.onjoined = function(kind, group, perms, status, data, error, message) {
+serverConnection.onjoined = function(kind, hall, perms, status, data, error, message) {
switch(kind) {
case 'join':
- this.request({'':['audio','video']});
+ this.request({'':['audio']});
// then update the UI, possibly taking perms.present into account
break;
case 'change':
@@ -113,7 +112,7 @@ serverConnection.onjoined = function(kind, group, perms, status, data, error, me
## Sending and receiving chat messages
-Once you have joined a group, you send chat messages with the `chat`
+Once you have joined a hall, you send chat messages with the `chat`
method of the `ServerConnection` class. No permission is needed to do that.
```javascript
@@ -133,11 +132,11 @@ chat messages, application-specific messages are not interpreted by the
server; unlike chat messages, they are not kept in the chat history.
The `useraction` method is used to ask the server to act on a remote user
-(kick it, change its permissions, etc.); similarly, the `groupaction`
-class requests an action to be performed on the current group. Most
+(kick it, change its permissions, etc.); similarly, the `hallaction`
+class requests an action to be performed on the current hall. Most
actions require either the `Op` or the `Record` permission.
-## Accepting incoming video streams
+## Accepting incoming audio streams
When the server pushes a stream to the client, the `ondownstream` callback
will trigger; you should set up the stream's callbacks here.
@@ -150,7 +149,7 @@ serverConnection.ondownstream = function(stream) {
}
```
-The `stream.label` field is one of `camera`, `screenshare` or `video`.
+The `stream.label` field is normally `audio`.
After a new stream is created, `ondowntrack` will be called whenever
a track is added.
@@ -166,11 +165,11 @@ parameter is true when the stream is being replaced by a new stream; in
that case, the call to `onclose` will be followed with a call to
`onstream` with the same `localId` value.
-## Pushing outgoing video streams
+## Pushing outgoing audio streams
If you have the `present` permission, you may use the `newUpStream` method
to push a stream to the server. Given a `MediaStream` called `localStream`
-(as obtained from `getUserMedia` or `getDisplayMedia`).
+(as obtained from `getUserMedia`).
```javascript
let stream = serverConnection.newUpStream();
diff --git a/skald-install.md b/skald-install.md
index b47a625..8fb563d 100644
--- a/skald-install.md
+++ b/skald-install.md
@@ -26,30 +26,6 @@ Olimex board, for example), you would say:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags='-s -w'
```
-### Optional: install libraries for background blur
-
-Skald's client uses Google's MediaPipe library to implement background
-blur. This library is optional, and if it is absent, Skald will
-disable the menu entries for background blur.
-
-Optionally install Google's MediaPipe library:
-
-```sh
-mkdir mediapipe
-cd mediapipe
-wget https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.21.tgz
-tar xzf tasks-vision-*.tgz
-rm -f ../static/third-party/tasks-vision
-mv package ../static/third-party/tasks-vision
-cd ../static/third-party/tasks-vision
-mkdir models
-cd models
-wget https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite
-cd ../../../../
-```
-
-If you don't have `wget` on your system, try using `curl -O` instead.
-
### Deploy to your server
The following instructions assume that your server is called
@@ -154,9 +130,9 @@ server's `data/` directory:
rsync config.json skald@skald.example.org:data/
```
-### Group setup
+### Hall setup
-Create a group:
+Create a hall:
```sh
skaldctl create-hall -hall city-watch
@@ -169,7 +145,7 @@ If you didn't install a TLS certificate above, you will need to run
skaldctl -insecure create-hall -hall city-watch
```
-Create an "op", a user with group moderation privileges:
+Create an "op", a user with hall moderation privileges:
```sh
skaldctl create-user -hall city-watch -user vimes -permissions op
diff --git a/skald-protocol.md b/skald-protocol.md
index 5d141fb..333bba9 100644
--- a/skald-protocol.md
+++ b/skald-protocol.md
@@ -2,9 +2,9 @@
## Data structures
-### Group
+### Hall
-A group is a set of clients. It is identified by a human-readable name
+A hall is a set of clients. It is identified by a human-readable name
that must not start or end with a slash "`/`", must not start with
a period "`.`", and must not contain the substrings "`/../`" or "`/./`".
@@ -28,24 +28,24 @@ server to client direction.
## Before connecting
-The client needs to know the location of the group, the (user-visible) URL
-at which the group is found. This may be obtained either by explicit
+The client needs to know the location of the hall, the (user-visible) URL
+at which the hall is found. This may be obtained either by explicit
configuration by the user, or by parsing the `/public-halls.json` file
-which may contain an array of group statuses (see below).
+which may contain an array of hall statuses (see below).
A client then performs an HTTP GET request on the file `.status` at
-the group's location. This yields a single JSON object, which contains
+the hall's location. This yields a single JSON object, which contains
the following fields:
- - `name`: the group's name
- - `location`: the group's location
+ - `name`: the hall's name
+ - `location`: the hall's location
- `endpoint`: the URL of the server's WebSocket endpoint
- `displayName`: a longer version of the name used for display;
- `description`: a user-readable description;
- `authServer`: the URL of the authentication server, if any;
- `authPortal`: the uRL of the authentication portal, if any;
- - `locked`: true if the group is locked;
- - `clientCount`: the number of clients currently in the group.
+ - `locked`: true if the hall is locked;
+ - `clientCount`: the number of clients currently in the hall.
All fields are optional except `name`, `location` and `endpoint`.
@@ -119,13 +119,13 @@ The receiving peer must reply with a `pong` message within 30s.
## Joining and leaving
-The `join` message requests that the sender join or leave a group:
+The `join` message requests that the sender join or leave a hall:
```javascript
{
type: 'join',
kind: 'join' or 'leave',
- group: group,
+ hall: hall,
username: username,
password: password,
data: data
@@ -135,7 +135,7 @@ The `join` message requests that the sender join or leave a group:
If token-based authorisation is beling used, then the `username` and
`password` fields are omitted, and a `token` field is included instead.
-When the sender has effectively joined the group, the peer will send
+When the sender has effectively joined the hall, the peer will send
a 'joined' message of kind 'join'; it may then send a 'joined' message of
kind 'change' at any time, in order to inform the client of a change in
its permissions or in the recommended RTC configuration.
@@ -145,7 +145,7 @@ its permissions or in the recommended RTC configuration.
type: 'joined',
kind: 'join' or 'fail' or 'change' or 'leave',
error: may be set if kind is 'fail',
- group: group,
+ hall: hall,
username: username,
permissions: permissions,
status: status,
@@ -157,12 +157,12 @@ its permissions or in the recommended RTC configuration.
The `username` field is the username that the server assigned to this
user. The `permissions` field is an array of strings that may contain the
values `present`, `op` and `record`. The `status` field is a dictionary
-that contains status information about the group, and updates the data
+that contains status information about the hall, and updates the data
obtained from the `.status` URL described above.
-## Maintaining group membership
+## Maintaining hall membership
-Whenever a user joins or leaves a group, the server will send all other
+Whenever a user joins or leaves a hall, the server will send all other
users a `user` message:
```javascript
@@ -188,15 +188,14 @@ A peer must explicitly request the streams that it wants to receive.
```
The field `request` is a dictionary that maps the labels of requested
-streams to a list containing either 'audio', or one of 'video' or
-'video-low'. The empty key `''` serves as default. For example:
+streams to a list containing `audio`. The empty key `''` serves as
+default. For example:
```javascript
{
type: 'request',
request: {
- camera: ['audio', 'video-low'],
- '': ['audio', 'video']
+ '': ['audio']
}
}
```
@@ -220,23 +219,16 @@ A stream is created by the sender with the `offer` message:
If a stream with the same id exists, then this is a renegotiation;
otherwise this message creates a new stream. If the field `replace` is
not empty, then this request additionally requests that an existing stream
-with the given id should be closed, and the new stream should replace it;
-this is used most notably when changing the simulcast envelope.
+with the given id should be closed, and the new stream should replace it.
-The field `label` is one of `camera`, `screenshare` or `video`, and will
-be matched against the keys sent by the receiver in their `request` message.
+The field `label` is normally `audio`, and will be matched against the
+keys sent by the receiver in their `request` message.
The field `sdp` contains the raw SDP string (i.e. the `sdp` field of
-a JSEP session description). Skald will interpret the `nack`,
-`nack pli`, `ccm fir` and `goog-remb` RTCP feedback types, and act
-accordingly.
+a JSEP session description). Skald is audio-only; video media sections
+are not accepted.
-The sender may either send a single stream per media section in the SDP,
-or use rid-based simulcasting with the streams ordered in decreasing order
-of throughput. In that case, it should send two video streams, the
-first one with high throughput, and the second one with throughput limited
-to roughly 100kbit/s. If more than two streams are sent, then only the
-first and the last one will be considered.
+The sender sends a single audio stream per media section in the SDP.
The receiver may either abort the stream immediately (see below), or send
an answer.
@@ -275,7 +267,7 @@ being offered by sending a 'requestStream' request:
{
type: 'requestStream'
id: id,
- request: [audio, video]
+ request: [audio]
}
```
@@ -329,7 +321,7 @@ The field `kind` can have one of the following values:
the `caption` permission).
If `dest` is empty, the message is a broadcast message, destined to all of
-the clients in the group. If `source` is empty, then the message was
+the clients in the hall. If `source` is empty, then the message was
originated by the server. The message is forwarded by the server without
interpretation, the server only validates that the `source` and `username`
fields are authentic. The field `privileged` is set to true by the server
@@ -359,7 +351,7 @@ chat history, and is not expected to contain user-visible content.
```
Currently defined kinds include `error`, `warning`, `info`, `kicked`,
-`clearchat` (not to be confused with the `clearchat` group action), and
+`clearchat` (not to be confused with the `clearchat` hall action), and
`mute`.
A user action requests that the server act upon a user.
@@ -377,11 +369,11 @@ A user action requests that the server act upon a user.
Currently defined kinds include `op`, `unop`, `present`, `unpresent`,
`kick` and `setdata`.
-Finally, a group action requests that the server act on the current group.
+Finally, a hall action requests that the server act on the current hall.
```javascript
{
- type: 'groupaction',
+ type: 'hallaction',
kind: kind,
source: source-id,
username: username,
@@ -391,7 +383,7 @@ Finally, a group action requests that the server act on the current group.
Currently defined kinds include `clearchat` (not to be confused with the
`clearchat` user message), `lock`, `unlock`, `record`, `unrecord`,
-`subgroups` and `setdata`.
+`subhalls` and `setdata`.
# Peer-to-peer file transfer protocol
@@ -552,8 +544,8 @@ Javascript code.
## Authentication server
-If a group's status dictionary has a non-empty `authServer` field, then
-the group uses an authentication server. Before joining, the client sends
+If a hall's status dictionary has a non-empty `authServer` field, then
+the hall uses an authentication server. Before joining, the client sends
a POST request to the authorisation server URL containing in its body
a JSON dictionary of the following form:
```javascript
@@ -564,7 +556,7 @@ a JSON dictionary of the following form:
}
```
-If the user is not allowed to join the group, then the authorisation
+If the user is not allowed to join the hall, then the authorisation
server replies with a code of 403 ("not authorised"), and Skald will
reject the user. If the authentication server has no opinion about
whether the user is allowed to join, it replies with a code of 204 ("no
@@ -587,16 +579,16 @@ the same format as in the `joined` message. Since the client will only
use the token once, at the very beginning of the session, the tokens
issued may have a short lifetime (on the order of 30s).
-A boolean `include-subgroups` claim may be included in the JWT payload. It
-has the same effect as the `include-subgroups` parameter used when generating
+A boolean `include-subhalls` claim may be included in the JWT payload. It
+has the same effect as the `include-subhalls` parameter used when generating
a stateful token.
## Authentication portal
-If a group's status dictionary has a non-empty `authPortal` field, Skald
+If a hall's status dictionary has a non-empty `authPortal` field, Skald
redirects the user agent to the URL indicated by `authPortal`. The
authentication portal performs authorisation, generates a token as above,
-then redirects back to the group's URL with the token stores in a URL
+then redirects back to the hall's URL with the token stores in a URL
query parameter named `token`:
https://skald.example.org/hall/hallname/?token=eyJhbG...
diff --git a/skald.md b/skald.md
index bc180fc..e9995bc 100644
--- a/skald.md
+++ b/skald.md
@@ -14,56 +14,49 @@ Skald is therefore accessed on URLs such as:
### The landing page
There is a landing page at the root of the server. The landing page
-contains a list of all groups marked "public" in their configuration, as
-well as a form that allows joining an arbitrary group.
+contains a list of all halls marked "public" in their configuration, as
+well as a form that allows joining an arbitrary hall.
Going through the landing page is not required: you are welcome to point
-your browser directly at the desired group URL.
+your browser directly at the desired hall URL.
-### The group pages
+### The hall pages
-A group named e.g. *city-watch* has an associated page
+A hall named e.g. *city-watch* has an associated page
`/hall/city-watch/`. After login, it presents an interface consisting
-of three panes:
+of a user list, chat, and connection controls:
- the left side pane contains the list of users who have joined the
- group; every username doubles as a menu of group and user actions;
- - the middle pane contains the chat messages published to the group;
- - the main pane, on the right, contains the videos being streamed by
- users in the group.
+ hall; every username doubles as a menu of hall and user actions;
+ - the chat pane contains the messages published to the hall;
+ - the controls area contains login, connection, microphone, and settings
+ controls.
-On mobile, only the latter pane is shown by default. An icon at the top
-left opens the user list, and an icon in the main pane switches to the
-chat.
+On mobile, the interface is collapsed to fit the smaller screen. The top
+controls expose the user list, chat, and settings.
### Buttons
There are up to three buttons at the top. The most important is the
-*Enable*/*Disable* button, which switches on or off both the camera and
-the microphone.
+*Enable*/*Disable* button, which switches the microphone on or off.
The *Mute* button mutes or unmutes the microphone; the microphone can be
-muted remotely by the group moderator, but it cannot be unmuted remotely.
-
-The *Share screen* button streams the contents of the screen or an
-individual window.
+muted remotely by the hall moderator, but it cannot be unmuted remotely.
### Side menu
-There is a menu on the right of the user interface. It allows switching
-off the camera (for audio-only sessions), choosing the camera and
-microphone, and setting the video throughput. The *Blackboard mode*
-checkbox increases resolution and sacrifices frame rate in favour of image
-quality. The *Play local file* dialogue streams a video from a local file.
+There is a menu on the right of the user interface. It allows choosing
+the microphone, selecting whether to receive audio, and adjusting audio
+processing options.
### User list
There is a user list on the left, starting with the current user and
-followed by all the users that have joined the current group.
+followed by all the users that have joined the current hall.
The user list doubles as a set of menus. Clicking on the current user (the
-first entry in the user list) opens the *group menu*, a menu with actions
-that apply to the group as a whole. Clicking on a different user opens
+first entry in the user list) opens the *hall menu*, a menu with actions
+that apply to the hall as a whole. Clicking on a different user opens
a *user menu*, a menu that applies to that specific user.
### Chat pane
@@ -85,16 +78,16 @@ commands.
### Inviting users
In order to generate an invitation link, choose the entry *Invite user* in
-the group menu. This generates a link of the form
+the hall menu. This generates a link of the form
https://skald.example.org:8443/hall/city-watch/?token=XXX
where the *XXX* part, known as the *token*, is a shared secret. Such
-a link allows password-less login to the group, and may therefore be
+a link allows password-less login to the hall, and may therefore be
shared e.g. over e-mail or instant messaging.
The invitation functionality is usually restricted to the moderator;
-however, groups may be configured with the `"unrestricted-tokens"` option,
+however, halls may be configured with the `"unrestricted-tokens"` option,
which allows all users to generate tokens.
Tokens can be created, modified, and expired using the `/invite`,
@@ -106,20 +99,20 @@ Skald includes a peer-to-peer, end-to-end encrypted file transfer protocol.
In order to transfer a file, click on the receiver's entry in the user
list and choose *Send file*.
-### Group moderation
+### Hall moderation
If a user has the *op* permission (short for *operator*), then they have
access to a number of moderation tools. The contextual menu that opens
when clicking on an entry in the user list is expanded with commands for
muting a user, sending them a warning, retrieving their IP address, or
-kicking them out from a group.
+kicking them out from a hall.
-The group menu (opened by clicking on one's own entry in the user's list)
-is extended with options to lock or to unlock a group (a locked group is
+The hall menu (opened by clicking on one's own entry in the user's list)
+is extended with options to lock or to unlock a hall (a locked hall is
one that non-operator users cannot join).
All of the moderation commands are also available as command-line commands
-(see above), which is helpful when moderating large groups.
+(see above), which is helpful when moderating large halls.
# Server administration
@@ -132,7 +125,7 @@ file may look as follows:
{
"users":{"vetinari": {"password":"lagniappe", "permissions": "admin"}},
"canonicalHost": "skald.example.org",
- "writableGroups": true
+ "writableHalls": true
}
```
@@ -147,7 +140,7 @@ or, better, with a hashed password:
}
},
"canonicalHost": "skald.example.org",
- "writableGroups": true
+ "writableHalls": true
}
```
@@ -156,10 +149,10 @@ manually edited at any time (there is no need to restart the server). The
fields are as follows:
- `users` defines the users allowed to administer the server, and has the
- same syntax as user definitions in groups (see below), except that the
+ same syntax as user definitions in halls (see below), except that the
only meaningful permission is `"admin"`;
- - `writableGroups`: if true, then the API used by `skaldctl` can be used
+ - `writableHalls`: if true, then the API used by `skaldctl` can be used
to modify hall definitions; if unset or false, then only read-only
access is allowed;
@@ -241,8 +234,8 @@ skaldctl create-user -hall city-watch -user vimes -permissions op
```
If the `-permissions` flag is not specified, it defaults to `present`,
-meaning that the user can participate in the chat and present videos to
-the group. Other useful values are `message`, which allows a user
+meaning that the user can participate in the chat and present audio to
+the hall. Other useful values are `message`, which allows a user
to participate in the chat only, and `observe`, which doesn't allow any
active participation.
@@ -276,7 +269,7 @@ skaldctl set-password -hall city-watch -wildcard -type wildcard
See the section *Client authorisation* below for more information about
password types.
-#### Automatic subgroups
+#### Automatic subhalls
It is sometimes necessary to create a large number of identical halls.
For example, the author has been using Skald to supervise computer
@@ -285,20 +278,20 @@ science practicals, where up to 40 students are working in pairs.
While it is possible to automate the creation of halls by accessing
Skald's API, by scripting calls to skaldctl, or by generating files
directly under `halls/`, Skald provides a facility called *automatic
-subgroups* that can be used to generate halls on demand.
+subhalls* that can be used to generate halls on demand.
-Automatic subgroups are enabled by setting the `"auto-subgroups"`
+Automatic subhalls are enabled by setting the `"auto-subhalls"`
field in the hall description:
```sh
-skaldctl create-hall unseen-university -auto-subgroups
+skaldctl create-hall unseen-university -auto-subhalls
```
-Whenever a user attempts to access a subgroup of `unseen-university`, for
-example `unseen-university/hex`, the group is created in memory and
+Whenever a user attempts to access a subhall of `unseen-university`, for
+example `unseen-university/hex`, the hall is created in memory and
persists until it is empty and its chat history has expired. The main
-hall's operator can view the list of populated subgroups with the command
-`/subgroups`.
+hall's operator can view the list of populated subhalls with the command
+`/subhalls`.
#### Managing tokens
@@ -312,19 +305,19 @@ skaldctl create-token -hall city-watch
skaldctl list-tokens -l -hall city-watch
```
-A token that is generated with the `-include-subgroups` flag applies to
+A token that is generated with the `-include-subhalls` flag applies to
the whole hierarchy rooted at the given hall, including both ordinary
-halls and automatically generated subgroups.
+halls and automatically generated subhalls.
```sh
-skaldctl create-token -hall city-watch -include-subgroups
+skaldctl create-token -hall city-watch -include-subhalls
```
Such a token can be attached to the root of the hall hierarchy, and
therefore be valid for any hall on the server:
```sh
-skaldctl create-token -hall '' -include-subgroups
+skaldctl create-token -hall '' -include-subhalls
```
### Hall description reference
@@ -346,50 +339,50 @@ are optional. The following fields are allowed:
- `authKeys`, `authServer` and `authPortal`: see *Authorisation* below;
- - `public`: if true, then the group is listed on the landing page;
+ - `public`: if true, then the hall is listed on the landing page;
- - `displayName`: a human-friendly version of the group name; this is
- displayed at the top of the group page;
+ - `displayName`: a human-friendly version of the hall name; this is
+ displayed at the top of the hall page;
- - `description`: a human-readable description of the group; this is
+ - `description`: a human-readable description of the hall; this is
displayed on the landing page for public halls;
- - `contact`: a human-readable contact for this group, such as an e-mail
+ - `contact`: a human-readable contact for this hall, such as an e-mail
address, ignored by the server;
- `comment`: a human-readable string, ignored by the server;
- - `max-clients`: the maximum number of clients that may join the group at
+ - `max-clients`: the maximum number of clients that may join the hall at
one time;
- `max-history-age`: the time, in seconds, during which chat history is
kept (default 14400, i.e. 4 hours);
- `not-before` and `expires`: the times (in ISO 8601 or RFC 3339 format)
- between which joining the group is allowed;
+ between which joining the hall is allowed;
- - `allow-recording`: if true, then recording is allowed in this group;
+ - `allow-recording`: if true, then recording is allowed in this hall;
- `unrestricted-tokens`: if true, then ordinary users (without the "op"
privilege) are allowed to create tokens;
- `allow-anonymous`: if true, then users may connect with an empty username;
- - `auto-subgroups`: if true, then subgroups of the form `group/subgroup`
+ - `auto-subhalls`: if true, then subhalls of the form `hall/subhall`
are automatically created when first accessed;
- - `autolock`: if true, the group will start locked and become locked
+ - `autolock`: if true, the hall will start locked and become locked
whenever there are no clients with operator privileges;
- `autokick`: if true, all clients will be kicked out whenever there are
no clients with operator privileges; this is not recommended, prefer
the `autolock` option instead;
- - `redirect`: if set, then attempts to join the group will be redirected
+ - `redirect`: if set, then attempts to join the hall will be redirected
to the given URL; most other fields are ignored in this case;
- - `codecs`: a list of codecs allowed in this group, see below for
- possible values. The default is `["vp8", "opus"]`.
+ - `codecs`: a list of codecs allowed in this hall, see below for
+ possible values. The default is `["opus"]`.
A user definition is a dictionary with entries `password` and
`permission`. The value of the `password` field is either a plaintext
@@ -398,30 +391,19 @@ hash-password` command. The value of the `permissions` field can either
be an array of individual permissions (not recommended), or one of the
following strings:
- - `op`: a group operator, with all rights except administering the group;
- - `present`, an ordinary user with the right to publish audio and video
- streams and send chat messages;
+ - `op`: a hall operator, with all rights except administering the hall;
+ - `present`, an ordinary user with the right to publish audio streams
+ and send chat messages;
- `message`: a user with the right to send chat messages;
- `observe`: a user that receives media streams and chat messages, but
is not allowed to send them;
- `caption`: a user with the right to display captions (only);
- - `admin`: a user with the right to administer the group (only).
+ - `admin`: a user with the right to administer the hall (only).
The value of the `codecs` field is an array of codecs allowed in the
-group. Supported video codecs include:
-
- - `"vp8"` (compatible with all supported browsers, full functionality);
- - `"vp9"` (better video quality, but incompatible with Safari; somewhat
- buggy in Firefox; full functionality);
- - `"av1"` (even better video quality, only supported by some browsers,
- limited functionality: no recording, no SVC);
- - `"h264"` (well supported by Apple devices, but incompatible with Debian
- Linux and with some older Android devices, SVC is not supported; might
- be covered by patents in some countries).
-
-Supported audio codecs include `"opus"`, `"g722"`, `"pcmu"` and `"pcma"`.
-Only Opus can be recorded to disk. There is no good reason to use
-anything except Opus.
+hall. Supported audio codecs include `"opus"`, `"g722"`, `"pcmu"` and
+`"pcma"`. Opus is the default and is the only codec planned for the
+single-file recording path.
## Client Authorisation
@@ -435,7 +417,7 @@ infrastructure (such as LDAP, OAuth2, or even Unix passwords).
### Password authorisation
When password authorisation is used, authorised usernames and passwords are
-defined directly in the group configuration file, in the `users` and
+defined directly in the hall configuration file, in the `users` and
`wildcard-user` entries. The `users` entry is a dictionary that maps user
names to user descriptions; the `wildcard-user` is a user description
that is used with usernames that don't appear in `users`. These two
@@ -521,7 +503,7 @@ authorisation decisions using cryptographic tokens generated by a third
party known as an *authorisation server*. Two authorisation servers are
available: an [LDAP client][2], and a [sample server written in Python][3].
-When an authorisation server is used, the `"authKeys"` entry of the group
+When an authorisation server is used, the `"authKeys"` entry of the hall
configuration file specifies one or more public keys in JWK format (with
the restriction that the "alg" key must be specified explicitly):
@@ -545,7 +527,7 @@ If multiple keys are provided, then they will all be tried in turn, unless
the token includes the "kid" header field, in which case only the
specified key will be used.
-The group file should also specify either an authorisation server or an
+The hall file should also specify either an authorisation server or an
authorisation portal. An authorisation server is specified using the
`"authServer"` key:
@@ -557,10 +539,10 @@ authorisation portal. An authorisation server is specified using the
If an authorisation server is specified, then the client, after it prompts
for a password, will request a token from the authorisation server and
-join the group using token authentication. The password is never
+join the hall using token authentication. The password is never
communicated to the server.
-Alternatively, the group file may specify an authorisation portal using
+Alternatively, the hall file may specify an authorisation portal using
the `"authPortal"` key. If an authorisation portal is specified, then the
default client will redirect initial client connections to the
authorisation portal. The authorisation portal is expected to authorise
diff --git a/skaldctl/skaldctl.go b/skaldctl/skaldctl.go
index 4f32a30..4ae1faa 100644
--- a/skaldctl/skaldctl.go
+++ b/skaldctl/skaldctl.go
@@ -66,23 +66,23 @@ var commands = map[string]command{
description: "delete a user's password",
},
"list-halls": {
- command: listGroupsCmd,
+ command: listHallsCmd,
description: "list halls",
},
"show-hall": {
- command: showGroupCmd,
+ command: showHallCmd,
description: "show hall definition",
},
"create-hall": {
- command: createGroupCmd,
+ command: createHallCmd,
description: "create a hall",
},
"update-hall": {
- command: updateGroupCmd,
+ command: updateHallCmd,
description: "change a hall's definition",
},
"delete-hall": {
- command: deleteGroupCmd,
+ command: deleteHallCmd,
description: "delete a hall",
},
"list-users": {
@@ -412,8 +412,8 @@ func initialSetupCmd(cmdname string, args []string) {
}
config := hall.Configuration{
- WritableGroups: true,
- Users: users,
+ WritableHalls: true,
+ Users: users,
}
encoder := json.NewEncoder(skaldConfig)
@@ -795,9 +795,9 @@ func stdinJSON(doit bool) (map[string]any, error) {
return data, nil
}
-func createGroupCmd(cmdname string, args []string) {
+func createHallCmd(cmdname string, args []string) {
var hallname string
- var unrestrictedTokens, autoSubgroups boolOption
+ var unrestrictedTokens, autoSubhalls boolOption
var doJSON bool
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
setUsage(cmd, cmdname,
@@ -808,8 +808,8 @@ func createGroupCmd(cmdname string, args []string) {
cmd.Var(&unrestrictedTokens, "unrestricted-tokens",
"allow ordinary users to create tokens",
)
- cmd.Var(&autoSubgroups, "auto-subgroups",
- "create subgroups automatically",
+ cmd.Var(&autoSubhalls, "auto-subhalls",
+ "create subhalls automatically",
)
cmd.BoolVar(&doJSON, "json", false,
"read JSON template from standard input",
@@ -842,8 +842,8 @@ func createGroupCmd(cmdname string, args []string) {
if unrestrictedTokens.set {
data["unrestricted-tokens"] = unrestrictedTokens.value
}
- if autoSubgroups.set {
- data["auto-subgroups"] = autoSubgroups.value
+ if autoSubhalls.set {
+ data["auto-subhalls"] = autoSubhalls.value
}
err = putJSON(u, data, false)
@@ -852,7 +852,7 @@ func createGroupCmd(cmdname string, args []string) {
}
}
-func deleteGroupCmd(cmdname string, args []string) {
+func deleteHallCmd(cmdname string, args []string) {
var hallname string
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
setUsage(cmd, cmdname,
@@ -886,9 +886,9 @@ func deleteGroupCmd(cmdname string, args []string) {
}
}
-func updateGroupCmd(cmdname string, args []string) {
+func updateHallCmd(cmdname string, args []string) {
var hallname string
- var unrestrictedTokens, autoSubgroups boolOption
+ var unrestrictedTokens, autoSubhalls boolOption
var doJSON bool
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
setUsage(cmd, cmdname,
@@ -899,8 +899,8 @@ func updateGroupCmd(cmdname string, args []string) {
cmd.Var(&unrestrictedTokens, "unrestricted-tokens",
"allow ordinary users to create tokens",
)
- cmd.Var(&autoSubgroups, "auto-subgroups",
- "create subgroups automatically",
+ cmd.Var(&autoSubhalls, "auto-subhalls",
+ "create subhalls automatically",
)
cmd.BoolVar(&doJSON, "json", false,
"read JSON template from standard input",
@@ -936,14 +936,14 @@ func updateGroupCmd(cmdname string, args []string) {
if unrestrictedTokens.set {
m["unrestricted-tokens"] = unrestrictedTokens.value
}
- if autoSubgroups.set {
- m["auto-subgroups"] = autoSubgroups.value
+ if autoSubhalls.set {
+ m["auto-subhalls"] = autoSubhalls.value
}
return m
})
if err != nil {
- log.Fatalf("Update group: %v", err)
+ log.Fatalf("Update hall: %v", err)
}
}
@@ -1269,7 +1269,7 @@ func deleteUserCmd(cmdname string, args []string) {
}
}
-func showGroupCmd(cmdname string, args []string) {
+func showHallCmd(cmdname string, args []string) {
var hallname string
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
setUsage(cmd, cmdname,
@@ -1306,7 +1306,7 @@ func showGroupCmd(cmdname string, args []string) {
}
}
-func listGroupsCmd(cmdname string, args []string) {
+func listHallsCmd(cmdname string, args []string) {
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
setUsage(cmd, cmdname,
"%v [option...] %v [pattern...]\n",
@@ -1320,17 +1320,17 @@ func listGroupsCmd(cmdname string, args []string) {
log.Fatalf("Build URL: %v", err)
}
- var groups []string
- _, err = getJSON(u, &groups)
+ var halls []string
+ _, err = getJSON(u, &halls)
if err != nil {
log.Fatalf("Get halls: %v", err)
}
- sort.Slice(groups, func(i, j int) bool {
- return groups[i] < groups[j]
+ sort.Slice(halls, func(i, j int) bool {
+ return halls[i] < halls[j]
})
- for _, g := range groups {
+ for _, h := range halls {
if len(patterns) > 0 {
- found, err := match(patterns, g)
+ found, err := match(patterns, h)
if err != nil {
log.Fatalf("Match: %v", err)
}
@@ -1338,7 +1338,7 @@ func listGroupsCmd(cmdname string, args []string) {
continue
}
}
- fmt.Println(g)
+ fmt.Println(h)
}
}
@@ -1409,7 +1409,7 @@ func listTokensCmd(cmdname string, args []string) {
exp = tt.Expires.Format(time.DateTime)
}
var perms []byte
- if tt.IncludeSubgroups {
+ if tt.IncludeSubhalls {
perms = append(perms, 'H')
}
for _, p := range tt.Permissions {
@@ -1432,13 +1432,13 @@ func listTokensCmd(cmdname string, args []string) {
func createTokenCmd(cmdname string, args []string) {
var hallname stringOption
var username, permissions string
- var includeSubgroups boolOption
+ var includeSubhalls boolOption
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
os.Args[0], cmdname,
)
cmd.Var(&hallname, "hall", "hall `name`")
- cmd.Var(&includeSubgroups, "include-subgroups", "include subgroups")
+ cmd.Var(&includeSubhalls, "include-subhalls", "include subhalls")
cmd.StringVar(&username, "user", "", "encode user `name` in token")
cmd.StringVar(&permissions, "permissions", "present", "permissions")
cmd.Parse(args)
@@ -1464,8 +1464,8 @@ func createTokenCmd(cmdname string, args []string) {
if username != "" {
t["username"] = username
}
- if includeSubgroups.set {
- t["includeSubgroups"] = includeSubgroups.value
+ if includeSubhalls.set {
+ t["includeSubhalls"] = includeSubhalls.value
}
u, err := url.JoinPath(
diff --git a/static/background-blur-worker.js b/static/background-blur-worker.js
deleted file mode 100644
index 789d690..0000000
--- a/static/background-blur-worker.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) 2024 by Juliusz Chroboczek.
-
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-// THE SOFTWARE.
-
-'use strict';
-
-let imageSegmenter;
-
-async function loadImageSegmenter(model) {
- let module = await import('/third-party/tasks-vision/vision_bundle.mjs');
- let vision = await module.FilesetResolver.forVisionTasks(
- "/third-party/tasks-vision/wasm"
- );
-
- return await module.ImageSegmenter.createFromOptions(vision, {
- baseOptions: {
- modelAssetPath: model,
- },
- outputCategoryMask: true,
- outputConfidenceMasks: false,
- runningMode: 'VIDEO',
- });
-}
-
-async function foregroundMask(bitmap, timestamp) {
- if(!(bitmap instanceof ImageBitmap))
- throw new Error('Bad type for worker data');
-
- try {
- let width = bitmap.width;
- let height = bitmap.height;
- let p = new Promise((resolve, reject) =>
- imageSegmenter.segmentForVideo(
- bitmap, timestamp,
- result => resolve(result),
- ));
- let result = await p;
- /** @type{Uint8Array} */
- let mask = result.categoryMask.getAsUint8Array();
- let id = new ImageData(width, height);
- for(let i = 0; i < mask.length; i++)
- id.data[4 * i + 3] = mask[i];
- result.close();
-
- let ib = await createImageBitmap(id);
- return {
- bitmap: bitmap,
- mask: ib,
- };
- } catch(e) {
- bitmap.close();
- throw(e);
- }
-}
-
-onmessage = async e => {
- try {
- let data = e.data;
- if(data.model) {
- if(imageSegmenter)
- throw new Error("image segmenter already initialised");
- imageSegmenter = await loadImageSegmenter(data.model);
- if(!imageSegmenter)
- throw new Error("loadImageSegmenter returned null");
- postMessage(null);
- } else if(data.bitmap) {
- if(imageSegmenter == null)
- throw new Error("image segmenter not initialised");
- let mask = await foregroundMask(data.bitmap, data.timestamp);
- postMessage(mask, [mask.bitmap, mask.mask]);
- } else {
- throw new Error("unexpected message type");
- }
- } catch(e) {
- postMessage(e);
- }
-}
diff --git a/static/change-password.js b/static/change-password.js
index 419d0c1..ff03131 100644
--- a/static/change-password.js
+++ b/static/change-password.js
@@ -24,9 +24,9 @@ document.getElementById('passwordform').onsubmit = async function(e) {
e.preventDefault();
let parms = new URLSearchParams(window.location.search);
- let group = parms.get('group');
- if(!group) {
- displayError("Couldn't determine group");
+ let hall = parms.get('hall');
+ if(!hall) {
+ displayError("Couldn't determine hall");
return;
}
let user = parms.get('username');
@@ -44,7 +44,7 @@ document.getElementById('passwordform').onsubmit = async function(e) {
}
try {
- await setPassword(group, user, false, new1, old);
+ await setPassword(hall, user, false, new1, old);
document.getElementById('old').value = '';
document.getElementById('new1').value = '';
document.getElementById('new2').value = '';
diff --git a/static/example/example.html b/static/example/example.html
index 9ab1717..35499c2 100644
--- a/static/example/example.html
+++ b/static/example/example.html
@@ -7,15 +7,10 @@
-
+
-
-
-
-
-
diff --git a/static/example/example.js b/static/example/example.js
index 5205b2c..b2be75a 100644
--- a/static/example/example.js
+++ b/static/example/example.js
@@ -1,4 +1,4 @@
-/* Skald client example. */
+/* Skald client example - audio-only. */
/**
* The main function.
@@ -6,7 +6,7 @@
* @param {string} url
*/
async function start(url) {
- // fetch the group information
+ // fetch the hall information
let r = await fetch(url + ".status");
if(!r.ok) {
throw new Error(`${r.status} ${r.statusText}`);
@@ -23,7 +23,7 @@ async function start(url) {
if(token) {
serverConnect(status, token);
} else if(status.authPortal) {
- window.location.href = groupStatus.authPortal
+ window.location.href = status.authPortal
return;
} else {
serverConnect(status, null);
@@ -33,7 +33,7 @@ async function start(url) {
/**
* Display the connection status.
*
- * @parm {string} status
+ * @param {string} status
*/
function displayStatus(status) {
let c = document.getElementById('status');
@@ -43,38 +43,29 @@ function displayStatus(status) {
/**
* Connect to the server.
*
- * @parm {Object} status
- * @parm {string} token
+ * @param {Object} status
+ * @param {string} token
*/
function serverConnect(status, token) {
- // create the connection to the server
let conn = new ServerConnection();
conn.onconnected = async function() {
displayStatus('Connected');
let creds = token ?
{type: 'token', token: token} :
{type: 'password', password: ''};
- // join the group and wait for the onjoined callback
await this.join("public", "example-user", creds);
};
conn.onchat = onChat;
conn.onusermessage = onUserMessage;
- conn.ondownstream = onDownStream;
conn.onclose = function() {
displayStatus('Disconnected');
}
conn.onjoined = onJoined;
-
- // connect and wait for the onconnected callback
conn.connect(status.endpoint);
}
/**
* Called whenever we receive a chat message.
- *
- * @this {ServerConnection}
- * @parm {string} username
- * @parm {string} message
*/
function onChat(id, dest, username, time, privileged, history, kind, message) {
let p = document.createElement('p');
@@ -85,11 +76,6 @@ function onChat(id, dest, username, time, privileged, history, kind, message) {
/**
* Called whenever we receive a user message.
- *
- * @this {ServerConnection}
- * @parm {string} username
- * @parm {string} message
- * @parm {string} kind
*/
function onUserMessage(id, dest, username, time, privileged, kind, error, message) {
switch(kind) {
@@ -113,57 +99,13 @@ function onUserMessage(id, dest, username, time, privileged, kind, error, messag
}
}
-
/**
- * Find the camera stream, if any.
- *
- * @parm {string} conn
- * @returns {Stream}
+ * Called when we join or leave a hall.
*/
-function cameraStream(conn) {
- for(let id in conn.up) {
- let s = conn.up[id];
- if(s.label === 'camera')
- return s;
- }
- return null;
-}
-
-/**
- * Enable or disable the show/hide button.
- *
- * @parm{ServerConnection} conn
- * @parm{boolean} enable
- */
-function enableShow(conn, enable) {
- let b = /** @type{HTMLButtonElement} */(document.getElementById('show'));
- if(enable) {
- b.onclick = function() {
- let s = cameraStream(conn);
- if(!s)
- showCamera(conn);
- else
- hide(conn, s);
- }
- b.disabled = false;
- } else {
- b.disabled = true;
- b.onclick = null;
- }
-}
-
-/**
- * Called when we join or leave a group.
- *
- * @this {ServerConnection}
- * @parm {string} kind
- * @parm {string} message}
- */
-async function onJoined(kind, group, perms, status, data, error, message) {
+async function onJoined(kind, hall, perms, status, data, error, message) {
switch(kind) {
case 'fail':
displayError(message);
- enableShow(this, false);
this.close();
break;
case 'redirect':
@@ -172,15 +114,13 @@ async function onJoined(kind, group, perms, status, data, error, message) {
return;
case 'leave':
displayStatus('Connected');
- enableShow(this, false);
this.close();
break;
case 'join':
case 'change':
- displayStatus(`Connected as ${this.username} in group ${this.group}.`);
- enableShow(this, true);
- // request videos from the server
- this.request({'': ['audio', 'video']});
+ displayStatus(`Connected as ${this.username} in hall ${this.hall}.`);
+ // request audio from the server
+ this.request({'': ['audio']});
break;
default:
displayError(`Unexpected state ${kind}.`);
@@ -189,115 +129,10 @@ async function onJoined(kind, group, perms, status, data, error, message) {
}
}
-/**
- * Create a video element. We encode the stream's id in the element's id
- * in order to avoid having a global hash table that maps ids to video
- * elements.
- *
- * @parm {string} id
- * @returns {HTMLVideoElement}
- */
-function makeVideoElement(id) {
- let v = document.createElement('video');
- v.id = 'video-' + id;
- let container = document.getElementById('videos');
- container.appendChild(v);
- return v;
-}
-
-/**
- * Find the video element that shows a given id.
- *
- * @parm {string} id
- * @returns {HTMLVideoElement}
- */
-function getVideoElement(id) {
- let v = document.getElementById('video-' + id);
- return /** @type{HTMLVideoElement} */(v);
-}
-
-/**
- * Enable the camera and broadcast yourself to the group.
- *
- * @parm {ServerConnection} conn
- */
-async function showCamera(conn) {
- let ms = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
-
- /* Send the new stream to the server */
- let s = conn.newUpStream();
- s.label = 'camera';
- s.setStream(ms);
- let v = makeVideoElement(s.localId);
- s.onclose = function(replace) {
- s.stream.getTracks().forEach(t => t.stop());
- v.srcObject = null;
- v.parentNode.removeChild(v);
- }
-
- function addTrack(t) {
- t.oneneded = function(e) {
- ms.onaddtrack = null;
- s.onremovetrack = null;
- s.close();
- }
- s.pc.addTransceiver(t, {
- direction: 'sendonly',
- streams: [ms],
- });
- }
-
- // Make sure all future tracks are added.
- s.onaddtrack = function(e) {
- addTrack(e.track);
- }
- // Add any existing tracks.
- ms.getTracks().forEach(addTrack);
-
- // Connect the MediaStream to the video element and start playing.
- v.srcObject = ms;
- v.muted = true;
- v.play();
-}
-
-/**
- * Stop broadcasting.
- *
- * @parm {ServerConnection} conn
- * @parm {Stream} s
- */
-async function hide(conn, s) {
- s.stream.getTracks().forEach(t => t.stop());
- s.close();
-}
-
-/**
- * Called when the server pushes a stream.
- *
- * @this {ServerConnection}
- * @parm {Stream} c
- */
-function onDownStream(s) {
- s.onclose = function(replace) {
- let v = getVideoElement(s.localId);
- v.srcObject = null;
- v.parentNode.removeChild(v);
- }
- s.ondowntrack = function(track, transceiver, stream) {
- let v = getVideoElement(s.localId);
- if(v.srcObject !== stream)
- v.srcObject = stream;
- }
-
- let v = makeVideoElement(s.localId);
- v.srcObject = s.stream;
- v.play();
-}
-
/**
* Display an error message.
*
- * @parm {string} message
+ * @param {string} message
*/
function displayError(message) {
document.getElementById('error').textContent = message;
diff --git a/static/index.html b/static/index.html
index dd43049..0737517 100644
--- a/static/index.html
+++ b/static/index.html
@@ -23,7 +23,7 @@
-
+
Public halls
diff --git a/static/mainpage.css b/static/mainpage.css
index 94dfe31..a7bf064 100644
--- a/static/mainpage.css
+++ b/static/mainpage.css
@@ -10,10 +10,10 @@ body {
height: 12px;
}
-.groups {
+.halls {
}
-.nogroups {
+.nohalls {
display: none;
}
diff --git a/static/mainpage.js b/static/mainpage.js
index 428e38d..a55dc81 100644
--- a/static/mainpage.js
+++ b/static/mainpage.js
@@ -23,17 +23,17 @@
document.getElementById('hallform').onsubmit = async function(e) {
e.preventDefault();
clearError();
- let groupinput = document.getElementById('hall')
+ let hallInput = document.getElementById('hall')
let button = document.getElementById('submitbutton');
- let group = groupinput.value.trim();
- if(group === '')
+ let hall = hallInput.value.trim();
+ if(hall === '')
return;
- let url = '/hall/' + group + '/';
+ let url = '/hall/' + hall + '/';
let statusUrl = url + '.status.json';
try {
- groupinput.disabled = true;
+ hallInput.disabled = true;
button.disabled = true;
try {
let resp = await fetch(statusUrl, {
@@ -41,7 +41,7 @@ document.getElementById('hallform').onsubmit = async function(e) {
});
if(!resp.ok) {
if(resp.status === 404)
- displayError('No such group');
+ displayError('No such hall');
else
displayError(`The server said: ${resp.status} ${resp.statusText}`);
return;
@@ -51,7 +51,7 @@ document.getElementById('hallform').onsubmit = async function(e) {
return;
}
} finally {
- groupinput.disabled = false;
+ hallInput.disabled = false;
button.disabled = false;
}
@@ -90,38 +90,38 @@ async function listPublicHalls() {
l = await r.json();
} catch(e) {
table.textContent = `Couldn't fetch halls: ${e}`;
- div.classList.remove('nogroups');
- div.classList.add('groups');
+ div.classList.remove('nohalls');
+ div.classList.add('halls');
return;
}
if (l.length === 0) {
table.textContent = '(No halls found.)';
- div.classList.remove('groups');
- div.classList.add('nogroups');
+ div.classList.remove('halls');
+ div.classList.add('nohalls');
return;
}
- div.classList.remove('nogroups');
- div.classList.add('groups');
+ div.classList.remove('nohalls');
+ div.classList.add('halls');
for(let i = 0; i < l.length; i++) {
- let group = l[i];
+ let hall = l[i];
let tr = document.createElement('tr');
let td = document.createElement('td');
let a = document.createElement('a');
- a.textContent = group.displayName || group.name;
- a.href = group.location;
+ a.textContent = hall.displayName || hall.name;
+ a.href = hall.location;
td.appendChild(a);
tr.appendChild(td);
let td2 = document.createElement('td');
- if(group.description)
- td2.textContent = group.description;
+ if(hall.description)
+ td2.textContent = hall.description;
tr.appendChild(td2);
let td3 = document.createElement('td');
- if(!group.redirect) {
- let locked = group.locked ? ', locked' : '';
- td3.textContent = `(${group.clientCount} clients${locked})`;
+ if(!hall.redirect) {
+ let locked = hall.locked ? ', locked' : '';
+ td3.textContent = `(${hall.clientCount} clients${locked})`;
} else {
td3.textContent = '(remote)';
}
diff --git a/static/management.js b/static/management.js
index d753f25..a7053ba 100644
--- a/static/management.js
+++ b/static/management.js
@@ -154,133 +154,133 @@ async function updateObject(url, values, etag) {
}
/**
- * listGroups returns the list of groups.
+ * listHalls returns the list of halls.
*
* @returns {Promise>}
*/
-async function listGroups() {
+async function listHalls() {
return await listObjects('/skald-api/v0/.halls/');
}
/**
- * getGroup returns the sanitised description of the given group.
+ * getHall returns the sanitised description of the given hall.
*
- * @param {string} group
+ * @param {string} hall
* @param {string} [etag]
* @returns {Promise