mirror of
https://github.com/jech/galene.git
synced 2024-11-22 08:35:57 +01:00
Allow various codecs.
It is now possible to specify codecs other than VP8 and Opus. This turns out not to be very useful, since VP8 is the only codec supported by all browsers (in violation of the WebRTC spec, which mandates support for H.264), and there is no good reason to use anything other than Opus for audio.
This commit is contained in:
parent
6c8e20c445
commit
5d38b0a231
6 changed files with 331 additions and 40 deletions
5
README
5
README
|
@ -139,6 +139,11 @@ fields, all of which are optional.
|
||||||
are automatically created when accessed.
|
are automatically created when accessed.
|
||||||
- `redirect`: if set, then attempts to join the group will be redirected
|
- `redirect`: if set, then attempts to join the group will be redirected
|
||||||
to the given URL; most other fields are ignored in this case.
|
to the given URL; most other fields are ignored in this case.
|
||||||
|
- `codecs`: this is a list of codecs allowed in this group. The default
|
||||||
|
is `["vp8", "opus"]`. Other possible values include `"vp9"`
|
||||||
|
(incompatible with Mac OS), `"h264"` (incompatible with some versions
|
||||||
|
of Firefox and Chromium), `"g722"`, `"pcmu"` and `"pcma"`. Recording
|
||||||
|
to disk is only supported for `"vp8"` and `"opus"`.
|
||||||
|
|
||||||
A user definition is a dictionary with the following fields:
|
A user definition is a dictionary with the following fields:
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ type Up interface {
|
||||||
DelLocal(Down) bool
|
DelLocal(Down) bool
|
||||||
Id() string
|
Id() string
|
||||||
Label() string
|
Label() string
|
||||||
|
Codecs() []webrtc.RTPCodecCapability
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type UpTrack represents a track in the client to server direction.
|
// Type UpTrack represents a track in the client to server direction.
|
||||||
|
|
181
group/group.go
181
group/group.go
|
@ -92,6 +92,7 @@ const (
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
name string
|
name string
|
||||||
|
api *webrtc.API
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
description *description
|
description *description
|
||||||
|
@ -146,11 +147,131 @@ func (g *Group) AllowRecording() bool {
|
||||||
var groups struct {
|
var groups struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
groups map[string]*Group
|
groups map[string]*Group
|
||||||
api *webrtc.API
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) API() *webrtc.API {
|
func (g *Group) API() *webrtc.API {
|
||||||
return groups.api
|
return g.api
|
||||||
|
}
|
||||||
|
|
||||||
|
func codecFromName(name string) (webrtc.RTPCodecCapability, error) {
|
||||||
|
switch name {
|
||||||
|
case "vp8":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"video/VP8", 90000, 0,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
case "vp9":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"video/VP9", 90000, 0,
|
||||||
|
"profile-id=2",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
case "h264":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"video/H264", 90000, 0,
|
||||||
|
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
case "opus":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"audio/opus", 48000, 2,
|
||||||
|
"minptime=10;useinbandfec=1",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
case "g722":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"audio/G722", 8000, 1,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
case "pcmu":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"audio/PCMU", 8000, 1,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
case "pcma":
|
||||||
|
return webrtc.RTPCodecCapability{
|
||||||
|
"audio/PCMA", 8000, 1,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return webrtc.RTPCodecCapability{}, errors.New("unknown codec")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadType(codec webrtc.RTPCodecCapability) (webrtc.PayloadType, error) {
|
||||||
|
switch strings.ToLower(codec.MimeType) {
|
||||||
|
case "video/vp8":
|
||||||
|
return 96, nil
|
||||||
|
case "video/vp9":
|
||||||
|
return 98, nil
|
||||||
|
case "video/h264":
|
||||||
|
return 102, nil
|
||||||
|
case "audio/opus":
|
||||||
|
return 111, nil
|
||||||
|
case "audio/g722":
|
||||||
|
return 9, nil
|
||||||
|
case "audio/pcmu":
|
||||||
|
return 0, nil
|
||||||
|
case "audio/pcma":
|
||||||
|
return 8, nil
|
||||||
|
default:
|
||||||
|
return 0, errors.New("unknown codec")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func APIFromCodecs(codecs []webrtc.RTPCodecCapability) *webrtc.API {
|
||||||
|
s := webrtc.SettingEngine{}
|
||||||
|
s.SetSRTPReplayProtectionWindow(512)
|
||||||
|
if !UseMDNS {
|
||||||
|
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||||
|
}
|
||||||
|
m := webrtc.MediaEngine{}
|
||||||
|
|
||||||
|
for _, codec := range codecs {
|
||||||
|
var tpe webrtc.RTPCodecType
|
||||||
|
var fb []webrtc.RTCPFeedback
|
||||||
|
if strings.HasPrefix(strings.ToLower(codec.MimeType), "video/") {
|
||||||
|
tpe = webrtc.RTPCodecTypeVideo
|
||||||
|
fb = []webrtc.RTCPFeedback{
|
||||||
|
{"goog-remb", ""},
|
||||||
|
{"nack", ""},
|
||||||
|
{"nack", "pli"},
|
||||||
|
{"ccm", "fir"},
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(strings.ToLower(codec.MimeType), "audio/") {
|
||||||
|
tpe = webrtc.RTPCodecTypeAudio
|
||||||
|
fb = []webrtc.RTCPFeedback{}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ptpe, err := payloadType(codec)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.RegisterCodec(
|
||||||
|
webrtc.RTPCodecParameters{
|
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
|
MimeType: codec.MimeType,
|
||||||
|
ClockRate: codec.ClockRate,
|
||||||
|
Channels: codec.Channels,
|
||||||
|
SDPFmtpLine: codec.SDPFmtpLine,
|
||||||
|
RTCPFeedback: fb,
|
||||||
|
},
|
||||||
|
PayloadType: ptpe,
|
||||||
|
},
|
||||||
|
tpe,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return webrtc.NewAPI(
|
||||||
|
webrtc.WithSettingEngine(s),
|
||||||
|
webrtc.WithMediaEngine(&m),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Add(name string, desc *description) (*Group, error) {
|
func Add(name string, desc *description) (*Group, error) {
|
||||||
|
@ -163,43 +284,6 @@ func Add(name string, desc *description) (*Group, error) {
|
||||||
|
|
||||||
if groups.groups == nil {
|
if groups.groups == nil {
|
||||||
groups.groups = make(map[string]*Group)
|
groups.groups = make(map[string]*Group)
|
||||||
s := webrtc.SettingEngine{}
|
|
||||||
s.SetSRTPReplayProtectionWindow(512)
|
|
||||||
if !UseMDNS {
|
|
||||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
|
||||||
}
|
|
||||||
m := webrtc.MediaEngine{}
|
|
||||||
m.RegisterCodec(
|
|
||||||
webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
"video/VP8", 90000, 0,
|
|
||||||
"",
|
|
||||||
[]webrtc.RTCPFeedback{
|
|
||||||
{"goog-remb", ""},
|
|
||||||
{"nack", ""},
|
|
||||||
{"nack", "pli"},
|
|
||||||
{"ccm", "fir"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecTypeVideo,
|
|
||||||
)
|
|
||||||
m.RegisterCodec(
|
|
||||||
webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
"audio/opus", 48000, 2,
|
|
||||||
"minptime=10;useinbandfec=1",
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
PayloadType: 111,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecTypeAudio,
|
|
||||||
)
|
|
||||||
groups.api = webrtc.NewAPI(
|
|
||||||
webrtc.WithSettingEngine(s),
|
|
||||||
webrtc.WithMediaEngine(&m),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -212,11 +296,29 @@ func Add(name string, desc *description) (*Group, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
names := desc.Codecs
|
||||||
|
if len(names) == 0 {
|
||||||
|
names = []string{"vp8", "opus"}
|
||||||
|
}
|
||||||
|
codecs := make([]webrtc.RTPCodecCapability, 0, len(names))
|
||||||
|
for _, n := range names {
|
||||||
|
codec, err := codecFromName(n)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Codec %v: %v", n, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
codecs = append(codecs, codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
api := APIFromCodecs(codecs)
|
||||||
|
|
||||||
g = &Group{
|
g = &Group{
|
||||||
name: name,
|
name: name,
|
||||||
description: desc,
|
description: desc,
|
||||||
clients: make(map[string]Client),
|
clients: make(map[string]Client),
|
||||||
timestamp: time.Now(),
|
timestamp: time.Now(),
|
||||||
|
api: api,
|
||||||
}
|
}
|
||||||
groups.groups[name] = g
|
groups.groups[name] = g
|
||||||
return g, nil
|
return g, nil
|
||||||
|
@ -592,6 +694,7 @@ type description struct {
|
||||||
Op []ClientCredentials `json:"op,omitempty"`
|
Op []ClientCredentials `json:"op,omitempty"`
|
||||||
Presenter []ClientCredentials `json:"presenter,omitempty"`
|
Presenter []ClientCredentials `json:"presenter,omitempty"`
|
||||||
Other []ClientCredentials `json:"other,omitempty"`
|
Other []ClientCredentials `json:"other,omitempty"`
|
||||||
|
Codecs []string `json:"codecs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMaxHistoryAge = 4 * time.Hour
|
const DefaultMaxHistoryAge = 4 * time.Hour
|
||||||
|
|
|
@ -46,3 +46,84 @@ func TestVP8Keyframe(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := isKeyframe("video/vp9", &packet)
|
||||||
|
if kf != (i == 0) || !kfKnown {
|
||||||
|
t.Errorf("isKeyframe(p%v): %v %v", i, kf, kfKnown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := isKeyframe("video/h264", &packet)
|
||||||
|
if kf != (i == 0) || !kfKnown {
|
||||||
|
t.Errorf("isKeyframe(p%v): %v %v", i, kf, kfKnown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -108,7 +108,8 @@ type rtpDownConnection struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDownConn(c group.Client, id string, remote conn.Up) (*rtpDownConnection, error) {
|
func newDownConn(c group.Client, id string, remote conn.Up) (*rtpDownConnection, error) {
|
||||||
pc, err := c.Group().API().NewPeerConnection(group.IceConfiguration())
|
api := group.APIFromCodecs(remote.Codecs())
|
||||||
|
pc, err := api.NewPeerConnection(group.IceConfiguration())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -300,6 +301,17 @@ func (up *rtpUpConnection) Label() string {
|
||||||
return up.label
|
return up.label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (up *rtpUpConnection) Codecs() []webrtc.RTPCodecCapability {
|
||||||
|
up.mu.Lock()
|
||||||
|
defer up.mu.Unlock()
|
||||||
|
|
||||||
|
codecs := make([]webrtc.RTPCodecCapability, len(up.tracks))
|
||||||
|
for i := range up.tracks {
|
||||||
|
codecs[i] = up.tracks[i].Codec()
|
||||||
|
}
|
||||||
|
return codecs
|
||||||
|
}
|
||||||
|
|
||||||
func (up *rtpUpConnection) AddLocal(local conn.Down) error {
|
func (up *rtpUpConnection) AddLocal(local conn.Down) error {
|
||||||
up.mu.Lock()
|
up.mu.Lock()
|
||||||
defer up.mu.Unlock()
|
defer up.mu.Unlock()
|
||||||
|
|
|
@ -30,6 +30,95 @@ func isKeyframe(codec string, packet *rtp.Packet) (bool, bool) {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
return false, true
|
return false, true
|
||||||
|
case "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 {
|
||||||
|
if (vp9.Payload[0] & 0x8) != 0 {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
return (vp9.Payload[0] & 0x4) == 0, true
|
||||||
|
} else {
|
||||||
|
if (vp9.Payload[0] & 0x4) != 0 {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
return (vp9.Payload[0] & 0x2) == 0, true
|
||||||
|
}
|
||||||
|
case "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 == 5, 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 == 5 {
|
||||||
|
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 == 5), true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue