Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
230 lines
6.0 KiB
Go
230 lines
6.0 KiB
Go
package uri
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// IsIP indicates if the URI host was specified using an IP address (v4 or v6).
|
|
func (a authorityInfo) IsIP() bool {
|
|
// IPvFuture won't parse as a netip.Addr
|
|
return a.isIPv4 || (a.isIPv6 && !a.isIPvFuture)
|
|
}
|
|
|
|
// IPAddr returns the parsed netip.Addr whenever IsIP is true (or the zero value whenever false).
|
|
func (a authorityInfo) IPAddr() netip.Addr {
|
|
if !a.IsIP() {
|
|
return netip.Addr{}
|
|
}
|
|
|
|
unescapedHost, _ := url.PathUnescape(a.host) // TODO
|
|
addr, _ := netip.ParseAddr(unescapedHost)
|
|
|
|
return addr
|
|
}
|
|
|
|
//nolint:dupword,mnd // false positive in the BNF comment
|
|
//nolint:mnd // straightforward interpretation, no need to define a constant
|
|
func validateIPv4(host string) error {
|
|
//
|
|
// check for IPv4 address
|
|
//
|
|
// The host SHOULD check
|
|
// the string syntactically for a dotted-decimal number before
|
|
// looking it up in the Domain Name System.
|
|
//
|
|
// IPv4 may **NOT** contain percent-encoded escaped characters, e.g. 192.168.0.%31 is not valid.
|
|
// Reference: https://www.rfc-editor.org/rfc/rfc3986#appendix-A
|
|
//
|
|
// IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
|
|
// dec-octet = DIGIT ; 0-9
|
|
// / %x31-39 DIGIT ; 10-99
|
|
// / "1" 2DIGIT ; 100-199
|
|
// / "2" %x30-34 DIGIT ; 200-249
|
|
// / "25" %x30-35 ; 250-255
|
|
var (
|
|
partNum, digitNum uint8
|
|
currentPart [3]byte
|
|
)
|
|
|
|
for i := 0; i < len(host); i++ {
|
|
b := host[i]
|
|
|
|
switch {
|
|
case isDigit(b):
|
|
switch digitNum {
|
|
case 0:
|
|
if b > '2' {
|
|
return errValueGreater255
|
|
}
|
|
case 1:
|
|
if currentPart[0] == '2' && b > '5' {
|
|
return errValueGreater255
|
|
}
|
|
case 2:
|
|
if currentPart[0] == '2' && currentPart[1] == '5' && b > '5' {
|
|
return errValueGreater255
|
|
}
|
|
default:
|
|
return errValueGreater255
|
|
}
|
|
|
|
currentPart[digitNum] = b
|
|
digitNum++
|
|
case b == '.':
|
|
if digitNum == 0 {
|
|
return errAtLeastOneDigit
|
|
}
|
|
if digitNum > 1 && currentPart[0] == '0' {
|
|
return errLeadingZero
|
|
}
|
|
|
|
partNum++
|
|
if partNum > 3 {
|
|
return errTooLong
|
|
}
|
|
digitNum = 0
|
|
default:
|
|
// optimize the default path to return a value error
|
|
return errInvalidCharacter
|
|
}
|
|
}
|
|
|
|
if partNum < 3 {
|
|
return errTooShort
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateIPv6(host string) error {
|
|
// * check IPv6
|
|
// IPv6address = 6( h16 ":" ) ls32
|
|
// / "::" 5( h16 ":" ) ls32
|
|
// / [ h16 ] "::" 4( h16 ":" ) ls32
|
|
// / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
|
|
// / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
|
|
// / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
|
|
// / [ *4( h16 ":" ) h16 ] "::" ls32
|
|
// / [ *5( h16 ":" ) h16 ] "::" h16
|
|
// / [ *6( h16 ":" ) h16 ] "::"
|
|
//
|
|
// ls32 = ( h16 ":" h16 ) / IPv4address
|
|
// ; least-significant 32 bits of address
|
|
//
|
|
// h16 = 1*4HEXDIG
|
|
// ; 16 bits of address represented in hexadecimal
|
|
// In addition to RFC 8986, RFC6874 states that IPv6 may include a zone ID like so:
|
|
// IPv6address *("%25" ZoneID}
|
|
// The zone ID may be percent-encoded
|
|
// ZoneID = 1*( unreserved / pct-encoded )
|
|
//
|
|
var ipv6WithoutZone, zoneID string
|
|
idx := strings.IndexRune(host, '%')
|
|
switch {
|
|
case idx == 0:
|
|
return errorsJoin(
|
|
ErrInvalidHostAddress,
|
|
fmt.Errorf("only the zoneID of an IPv6 literal may be percent-encoded: %w", ErrURI),
|
|
)
|
|
case idx > 0:
|
|
ipv6WithoutZone = host[:idx]
|
|
zoneID = host[idx:]
|
|
default:
|
|
ipv6WithoutZone = host
|
|
}
|
|
|
|
// check for IPv6 address
|
|
// IPv6 may contain a percent-encoded escaped zone identifier: unescape it first if present
|
|
addr, err := netip.ParseAddr(ipv6WithoutZone)
|
|
if err != nil {
|
|
return errorsJoin(
|
|
ErrInvalidHostAddress,
|
|
fmt.Errorf("a square-bracketed host part should be a valid IPv6 address: %q: %w", host, ErrURI),
|
|
)
|
|
}
|
|
|
|
if !addr.Is6() {
|
|
// RFC3986 stipulates that only IPv6 addresses are within square brackets
|
|
return errorsJoin(
|
|
ErrInvalidHostAddress,
|
|
fmt.Errorf("a square-bracketed host part should not contain an IPv4 address: %q: %w", host, ErrURI),
|
|
)
|
|
}
|
|
|
|
if zoneID == "" {
|
|
return nil
|
|
}
|
|
|
|
// check the escaping of the zoneID. Notice than if a zone is added, it must not be empty.
|
|
if len(zoneID) < 4 || zoneID[1] != '2' || zoneID[2] != '5' {
|
|
return errorsJoin(
|
|
ErrInvalidHostAddress,
|
|
fmt.Errorf("IPv6 zoneID separator in URI must be percent-encoded with %%25, but got: %q: %w", zoneID, ErrURI),
|
|
)
|
|
}
|
|
|
|
if err := validateUnreservedWithExtra(zoneID, nil); err != nil {
|
|
return errorsJoin(
|
|
ErrInvalidHostAddress,
|
|
fmt.Errorf("invalid IPv6 zoneID %q: %w", zoneID, err),
|
|
ErrURI,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateIPvFuture covers the special provision in the RFC for future IP scheme.
|
|
// The passed argument removes the heading "v" character.
|
|
//
|
|
// Example: http://[v6.fe80::a_en1]
|
|
//
|
|
// Reference: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
|
|
//
|
|
// IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
|
|
func validateIPvFuture(address string) error {
|
|
var (
|
|
offset int
|
|
foundHexDigits, foundDot bool
|
|
)
|
|
|
|
for offset < len(address) {
|
|
r, size := utf8.DecodeRuneInString(address[offset:])
|
|
if r == utf8.RuneError {
|
|
return fmt.Errorf("invalid UTF8 rune near: %q: %w", address[offset:], ErrURI)
|
|
}
|
|
offset += size
|
|
|
|
if r == dotSeparator {
|
|
foundDot = true
|
|
|
|
break
|
|
}
|
|
|
|
if !isHex(r) {
|
|
return fmt.Errorf(
|
|
"invalid IP vFuture format: expect an hexadecimal version tag: %w", ErrURI,
|
|
)
|
|
}
|
|
|
|
foundHexDigits = true
|
|
}
|
|
|
|
if !foundHexDigits || !foundDot {
|
|
return fmt.Errorf(
|
|
"invalid IP vFuture format: expect a '.' after an hexadecimal version tag: %w", ErrURI,
|
|
)
|
|
}
|
|
|
|
if offset >= len(address) {
|
|
return fmt.Errorf("invalid IP vFuture format: expect a non-empty address after the version tag: %w", ErrURI)
|
|
}
|
|
|
|
// TODO: wrong because IpvFuture is not escaped
|
|
return validateUnreservedWithExtra(address[offset:], userInfoExtraRunes)
|
|
}
|