diff --git a/stream.go b/stream.go index fa5b2bf..22b15f6 100644 --- a/stream.go +++ b/stream.go @@ -70,9 +70,12 @@ func Open(url string) (*Stream, error) { log.Print("[DEBUG] HTTP header ", k, ": ", v[0]) } - bitrate, err := strconv.Atoi(resp.Header.Get("icy-br")) - if err != nil { - return nil, fmt.Errorf("cannot parse bitrate: %v", err) + var bitrate int + if rawBitrate := resp.Header.Get("icy-br"); rawBitrate != "" { + bitrate, err = strconv.Atoi(rawBitrate) + if err != nil { + return nil, fmt.Errorf("cannot parse bitrate: %v", err) + } } metaint, err := strconv.Atoi(resp.Header.Get("icy-metaint")) @@ -96,35 +99,31 @@ func Open(url string) (*Stream, error) { } // Read implements the standard Read interface -func (s *Stream) Read(p []byte) (n int, err error) { - n, err = s.rc.Read(p) - - if s.pos+n <= s.metaint { - s.pos = s.pos + n - return n, err - } - - // extract stream metadata - metadataStart := s.metaint - s.pos - metadataLength := int(p[metadataStart : metadataStart+1][0]) * 16 - if metadataLength > 0 { - m := NewMetadata(p[metadataStart+1 : metadataStart+1+metadataLength]) - if !m.Equals(s.metadata) { - s.metadata = m - if s.MetadataCallbackFunc != nil { - s.MetadataCallbackFunc(s.metadata) - } +func (s *Stream) Read(buf []byte) (dataLen int, err error) { + dataLen, err = s.rc.Read(buf) + + checkedDataLen := 0 + uncheckedDataLen := dataLen + for s.pos+uncheckedDataLen > s.metaint { + offset := s.metaint - s.pos + skip, e := s.extractMetadata(buf[checkedDataLen+offset:]) + if e != nil { + err = e + } + s.pos = 0 + if offset+skip > uncheckedDataLen { + dataLen = checkedDataLen + offset + uncheckedDataLen = 0 + } else { + checkedDataLen += offset + dataLen -= skip + uncheckedDataLen = dataLen - checkedDataLen + copy(buf[checkedDataLen:], buf[checkedDataLen+skip:]) } } + s.pos = s.pos + uncheckedDataLen - // roll over position + metadata block - s.pos = ((s.pos + n) - s.metaint) - metadataLength - 1 - - // shift buffer data to account for metadata block - copy(p[metadataStart:], p[metadataStart+1+metadataLength:]) - n = n - 1 - metadataLength - - return n, err + return } // Close closes the stream @@ -132,3 +131,42 @@ func (s *Stream) Close() error { log.Print("[INFO] Closing ", s.URL) return s.rc.Close() } + +func (s *Stream) extractMetadata(p []byte) (int, error) { + var metabuf []byte + var err error + length := int(p[0]) * 16 + end := length + 1 + complete := false + if length > 0 { + if len(p) < end { + // The provided buffer was not large enough for the metadata block to fit in. + // Read whole metadata into our own buffer. + metabuf = make([]byte, length) + copy(metabuf, p[1:]) + n := len(p) - 1 + for n < length && err == nil { + var nn int + nn, err = s.rc.Read(metabuf[n:]) + n += nn + } + if n == length { + complete = true + } else if err == nil || err == io.EOF { + err = io.ErrUnexpectedEOF + } + } else { + metabuf = p[1:end] + complete = true + } + } + if complete { + if m := NewMetadata(metabuf); !m.Equals(s.metadata) { + s.metadata = m + if s.MetadataCallbackFunc != nil { + s.MetadataCallbackFunc(s.metadata) + } + } + } + return length + 1, err +} diff --git a/stream_test.go b/stream_test.go index f7f5084..8022fe9 100644 --- a/stream_test.go +++ b/stream_test.go @@ -1,7 +1,6 @@ package shoutcast import ( - "fmt" "io" "math" "net/http" @@ -17,11 +16,38 @@ func assertStrings(t *testing.T, got, want string) { } } -func assertEqual(t *testing.T, got, want interface{}) { +func assertEqual(t *testing.T, got, want interface{}) bool { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %+v, want %+v", got, want) + return false } + return true +} + +func assertNoError(t *testing.T, err error) bool { + t.Helper() + if err != nil { + t.Errorf("Received unexpected error:\n%+v", err) + return false + } + return true +} + +func requireNoError(t *testing.T, err error) { + t.Helper() + if !assertNoError(t, err) { + t.FailNow() + } +} + +func assertError(t *testing.T, err error) bool { + t.Helper() + if err == nil { + t.Error("An error is expected but got nil.") + return false + } + return true } func makeMetadata(s string) []byte { @@ -73,6 +99,16 @@ func TestRequiredHTTPHeadersArePresent(t *testing.T) { assertStrings(t, headers.Get("user-agent")[:6], "iTunes") } +func TestMissingBitrate(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header()["icy-metaint"] = []string{"100"} + w.WriteHeader(200) + })) + + _, err := Open(ts.URL) + assertNoError(t, err) +} + func TestUnexpectedEOF(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("icy-br", "192") @@ -80,25 +116,39 @@ func TestUnexpectedEOF(t *testing.T) { metadata := makeMetadata("SongTitle='Prospa Prayer';") stream := insertMetadata([]byte{1, 1}, metadata, 1) - fmt.Printf("%v\n", stream) - w.Write(stream) + // unexpected EOF in the middle of a metadata block + w.Write(stream[:len(stream)-10]) })) defer ts.Close() s, _ := Open(ts.URL) b1 := make([]byte, 1) - s.Read(b1) - assertEqual(t, b1, []byte{1}) + n, err := s.Read(b1) + if assertNoError(t, err) && assertEqual(t, 1, n) { + assertEqual(t, []byte{1}, b1) + } + + // The metadata is read immediately and does not fit into the buffer. + // -> `0, nil` is returned. + // Filling the buffer after the reading of the metadata would be more complexity without advantage. + n, err = s.Read(b1) + assertNoError(t, err) + assertEqual(t, 0, n) b2 := make([]byte, 1) - s.Read(b2) - assertEqual(t, b2, []byte{1}) + n, err = s.Read(b2) + if assertNoError(t, err) && assertEqual(t, 1, n) { + assertEqual(t, []byte{1}, b2) + } - // ooops, nothing to read + // oops, nothing to read b3 := make([]byte, 1) - _, err := s.Read(b3) - assertEqual(t, err, io.ErrUnexpectedEOF) + n, err = s.Read(b3) + assertEqual(t, 0, n) + if assertError(t, err) { + assertEqual(t, io.ErrUnexpectedEOF, err) + } } func TestMetaintEqualsClientBufferLength(t *testing.T) { @@ -108,7 +158,6 @@ func TestMetaintEqualsClientBufferLength(t *testing.T) { metadata := makeMetadata("SongTitle='Prospa Prayer';") stream := insertMetadata([]byte{1, 1, 1, 1, 1, 1}, metadata, 2) - fmt.Printf("%v\n", stream) w.Write(stream) })) defer ts.Close() @@ -116,16 +165,41 @@ func TestMetaintEqualsClientBufferLength(t *testing.T) { s, _ := Open(ts.URL) b1 := make([]byte, 2) - s.Read(b1) - assertEqual(t, b1, []byte{1, 1}) + n, err := s.Read(b1) + if assertNoError(t, err) && assertEqual(t, 2, n) { + assertEqual(t, []byte{1, 1}, b1) + } + + // The metadata is read immediately and does not fit into the buffer. + // -> `0, nil` is returned. + // Filling the buffer after the reading of the metadata would be more complexity without advantage. + n, err = s.Read(b1) + assertNoError(t, err) + assertEqual(t, 0, n) b2 := make([]byte, 2) - s.Read(b2) - assertEqual(t, b2, []byte{1, 1}) + n, err = s.Read(b2) + if assertNoError(t, err) && assertEqual(t, 2, n) { + assertEqual(t, []byte{1, 1}, b2) + } + + // no data except metadata read, again + n, err = s.Read(b2) + assertNoError(t, err) + assertEqual(t, 0, n) b3 := make([]byte, 2) - s.Read(b3) - assertEqual(t, b3, []byte{1, 1}) + n, err = s.Read(b3) + if assertNoError(t, err) && assertEqual(t, 2, n) { + assertEqual(t, []byte{1, 1}, b3) + } + + // check for EOF + n, err = s.Read(b1) + assertEqual(t, 0, n) + if assertError(t, err) { + assertEqual(t, io.EOF, err) + } } func TestMetaintGreaterThanClientBufferLength(t *testing.T) { @@ -135,7 +209,6 @@ func TestMetaintGreaterThanClientBufferLength(t *testing.T) { metadata := makeMetadata("SongTitle='Prospa Prayer';") stream := insertMetadata([]byte{1, 1, 1, 1, 1, 1}, metadata, 3) - fmt.Printf("%v\n", stream) w.Write(stream) })) defer ts.Close() @@ -143,16 +216,97 @@ func TestMetaintGreaterThanClientBufferLength(t *testing.T) { s, _ := Open(ts.URL) b1 := make([]byte, 2) - s.Read(b1) - assertEqual(t, b1, []byte{1, 1}) + n, err := s.Read(b1) + if assertNoError(t, err) && assertEqual(t, 2, n) { + assertEqual(t, []byte{1, 1}, b1) + } + // only one byte read then follows metadata b2 := make([]byte, 2) - s.Read(b2) - assertEqual(t, b2, []byte{1, 1}) + n, err = s.Read(b2) + if assertNoError(t, err) && assertEqual(t, 1, n) { + // don't assert the second byte, only one read + assertEqual(t, []byte{1}, b2[:1]) + } b3 := make([]byte, 2) - s.Read(b3) - assertEqual(t, b3, []byte{1, 1}) + n, err = s.Read(b3) + if assertNoError(t, err) && assertEqual(t, 2, n) { + assertEqual(t, []byte{1, 1}, b3) + } + + // only one byte read then follows metadata and then EOF + b4 := make([]byte, 2) + n, err = s.Read(b4) + if assertEqual(t, 1, n) { + // don't assert the second byte, only one read + assertEqual(t, []byte{1}, b4[:1]) + } + if assertError(t, err) { + assertEqual(t, io.EOF, err) + } +} + +func TestClientBufferLargeEnoughForMetadata(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("icy-br", "192") + w.Header().Set("icy-metaint", "3") + + metadata := makeMetadata("SongTitle='Prospa Prayer';") + stream := insertMetadata([]byte{3, 4, 5, 6, 7, 8, 9}, metadata, 3) + w.Write(stream) + })) + defer ts.Close() + + s, err := Open(ts.URL) + requireNoError(t, err) + + // metadata length is 33 (2*16+1) -> 38 - 33 = 5 bytes stream data to be read + b1 := make([]byte, 38) + n, err := s.Read(b1) + if assertNoError(t, err) && assertEqual(t, 5, n) { + assertEqual(t, []byte{3, 4, 5, 6, 7}, b1[:5]) + } + + b2 := make([]byte, 38) + n, err = s.Read(b2) + if assertEqual(t, 2, n) { + assertEqual(t, []byte{8, 9}, b2[:2]) + } + if assertError(t, err) { + assertEqual(t, io.EOF, err) + } +} + +func TestClientBufferLargeEnoughForTwoTimesMetadata(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("icy-br", "192") + w.Header().Set("icy-metaint", "3") + + metadata := makeMetadata("SongTitle='Prospa Prayer';") + stream := insertMetadata([]byte{3, 4, 5, 6, 7, 8, 9, 10}, metadata, 3) + w.Write(stream) + })) + defer ts.Close() + + s, err := Open(ts.URL) + requireNoError(t, err) + + // metadata length is 33 (2*16+1) -> 73 - 2 * 33 = 7 + b1 := make([]byte, 73) + n, err := s.Read(b1) + if assertNoError(t, err) && assertEqual(t, 7, n) { + assertEqual(t, []byte{3, 4, 5, 6, 7, 8, 9}, b1[:7]) + } + + b2 := make([]byte, 38) + n, err = s.Read(b2) + if assertEqual(t, 1, n) { + assertEqual(t, []byte{10}, b2[:1]) + } + if assertError(t, err) { + assertEqual(t, io.EOF, err) + } } // test for EOF