Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

146 linhas
3.1KB

  1. package icecast
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "strconv"
  7. "strings"
  8. )
  9. type icyMetadata struct {
  10. StreamTitle string
  11. }
  12. type icyReader struct {
  13. r io.Reader
  14. metaInt int
  15. audioLeft int
  16. onMetadata func(icyMetadata)
  17. }
  18. func newICYReader(r io.Reader, metaInt int, onMetadata func(icyMetadata)) io.Reader {
  19. if r == nil || metaInt <= 0 {
  20. return r
  21. }
  22. return &icyReader{
  23. r: r,
  24. metaInt: metaInt,
  25. audioLeft: metaInt,
  26. onMetadata: onMetadata,
  27. }
  28. }
  29. func (r *icyReader) Read(p []byte) (int, error) {
  30. if len(p) == 0 {
  31. return 0, nil
  32. }
  33. for {
  34. if r.audioLeft == 0 {
  35. if err := r.readMetadataBlock(); err != nil {
  36. return 0, err
  37. }
  38. r.audioLeft = r.metaInt
  39. continue
  40. }
  41. want := len(p)
  42. if want > r.audioLeft {
  43. want = r.audioLeft
  44. }
  45. n, err := r.r.Read(p[:want])
  46. if n > 0 {
  47. r.audioLeft -= n
  48. return n, nil
  49. }
  50. if err != nil {
  51. return 0, err
  52. }
  53. }
  54. }
  55. func (r *icyReader) readMetadataBlock() error {
  56. var lenBuf [1]byte
  57. if _, err := io.ReadFull(r.r, lenBuf[:]); err != nil {
  58. return err
  59. }
  60. blockLen := int(lenBuf[0]) * 16
  61. if blockLen == 0 {
  62. return nil
  63. }
  64. block := make([]byte, blockLen)
  65. if _, err := io.ReadFull(r.r, block); err != nil {
  66. return err
  67. }
  68. if r.onMetadata != nil {
  69. r.onMetadata(parseICYMetadata(block))
  70. }
  71. return nil
  72. }
  73. // parseICYMetadata parses the ICY inline metadata block.
  74. //
  75. // ICY metadata is a semicolon-delimited key=value format where values are
  76. // single-quoted strings. A naive strings.Split(raw, ";") breaks when the
  77. // StreamTitle itself contains semicolons (e.g. "Artist - Title; Live Edit").
  78. // This parser is quote-aware: it only splits on semicolons that appear
  79. // outside of single-quoted value strings.
  80. func parseICYMetadata(block []byte) icyMetadata {
  81. raw := strings.TrimRight(string(bytes.Trim(block, "\x00")), "\x00")
  82. meta := icyMetadata{}
  83. fields := splitICYFields(raw)
  84. for _, field := range fields {
  85. field = strings.TrimSpace(field)
  86. if !strings.HasPrefix(field, "StreamTitle=") {
  87. continue
  88. }
  89. v := strings.TrimPrefix(field, "StreamTitle=")
  90. v = strings.TrimSpace(v)
  91. // Strip enclosing single or double quotes.
  92. if len(v) >= 2 {
  93. if (v[0] == '\'' && v[len(v)-1] == '\'') ||
  94. (v[0] == '"' && v[len(v)-1] == '"') {
  95. v = v[1 : len(v)-1]
  96. }
  97. }
  98. meta.StreamTitle = v
  99. break
  100. }
  101. return meta
  102. }
  103. // splitICYFields splits an ICY metadata string on semicolons that appear
  104. // outside of single-quoted value strings. Semicolons inside quotes (e.g.
  105. // StreamTitle='Artist - Song; Live';) are preserved as part of the value.
  106. func splitICYFields(s string) []string {
  107. var fields []string
  108. inQuote := false
  109. start := 0
  110. for i := 0; i < len(s); i++ {
  111. c := s[i]
  112. if c == '\'' {
  113. inQuote = !inQuote
  114. }
  115. if c == ';' && !inQuote {
  116. fields = append(fields, s[start:i])
  117. start = i + 1
  118. }
  119. }
  120. if start < len(s) {
  121. fields = append(fields, s[start:])
  122. }
  123. return fields
  124. }
  125. func parseICYMetaInt(raw string) (int, error) {
  126. raw = strings.TrimSpace(raw)
  127. if raw == "" {
  128. return 0, nil
  129. }
  130. n, err := strconv.Atoi(raw)
  131. if err != nil || n < 0 {
  132. return 0, fmt.Errorf("invalid icy-metaint: %q", raw)
  133. }
  134. return n, nil
  135. }