diff --git a/README.md b/README.md
index 36cb894..4efb502 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Features:
 * Client
 
   * Read streams in MPEG-TS, fMP4 or Low-latency format
-  * Read a single video track and/or a single audio track
+  * Read a single video track and/or multiple audio tracks
   * Read tracks encoded with AV1, VP9, H265, H264, Opus, MPEG-4 Audio (AAC)
   * Get absolute timestamp of incoming data
 
diff --git a/client.go b/client.go
index 7d16d26..126e16a 100644
--- a/client.go
+++ b/client.go
@@ -62,10 +62,6 @@ type ClientOnDataMPEG4AudioFunc func(pts int64, aus [][]byte)
 // ClientOnDataOpusFunc is the prototype of the function passed to OnDataOpus().
 type ClientOnDataOpusFunc func(pts int64, packets [][]byte)
 
-type clientOnStreamTracksFunc func(ctx context.Context, isLeading bool, tracks []*Track) ([]*clientTrack, bool)
-
-type clientOnDataFunc func(pts int64, dts int64, data [][]byte)
-
 func clientAbsoluteURL(base *url.URL, relative string) (*url.URL, error) {
 	u, err := url.Parse(relative)
 	if err != nil {
diff --git a/client_primary_downloader.go b/client_primary_downloader.go
index a6a107a..54326d2 100644
--- a/client_primary_downloader.go
+++ b/client_primary_downloader.go
@@ -40,7 +40,7 @@ func cloneURL(ur *url.URL) *url.URL {
 	}
 }
 
-func clientDownloadPlaylist(
+func downloadPlaylist(
 	ctx context.Context,
 	httpClient *http.Client,
 	onRequest ClientOnRequestFunc,
@@ -94,34 +94,19 @@ func pickLeadingPlaylist(variants []*playlist.MultivariantVariant) *playlist.Mul
 	return leadingPlaylist
 }
 
-func pickAudioPlaylist(alternatives []*playlist.MultivariantRendition, groupID string) *playlist.MultivariantRendition {
-	candidates := func() []*playlist.MultivariantRendition {
-		var ret []*playlist.MultivariantRendition
-		for _, alt := range alternatives {
-			if alt.GroupID == groupID {
-				ret = append(ret, alt)
-			}
-		}
-		return ret
-	}()
-	if candidates == nil {
-		return nil
-	}
+func getRenditionsByGroup(
+	renditions []*playlist.MultivariantRendition,
+	groupID string,
+) []*playlist.MultivariantRendition {
+	var ret []*playlist.MultivariantRendition
 
-	// pick the default audio playlist
-	for _, alt := range candidates {
-		if alt.Default {
-			return alt
+	for _, alt := range renditions {
+		if alt.GroupID == groupID {
+			ret = append(ret, alt)
 		}
 	}
 
-	// alternatively, pick the first one
-	return candidates[0]
-}
-
-type streamTracksEntry struct {
-	isLeading bool
-	tracks    []*Track
+	return ret
 }
 
 type clientPrimaryDownloader struct {
@@ -139,34 +124,24 @@ type clientPrimaryDownloader struct {
 	getLeadingTimeConv        func(ctx context.Context) (clientTimeConv, bool)
 
 	clientTracks map[*Track]*clientTrack
-
-	// in
-	chStreamTracks chan streamTracksEntry
-	chStreamEnded  chan struct{}
-
-	// out
-	startStreaming chan struct{}
 }
 
 func (d *clientPrimaryDownloader) initialize() {
-	d.chStreamTracks = make(chan streamTracksEntry)
-	d.chStreamEnded = make(chan struct{})
-	d.startStreaming = make(chan struct{})
 }
 
 func (d *clientPrimaryDownloader) run(ctx context.Context) error {
 	d.onDownloadPrimaryPlaylist(d.primaryPlaylistURL.String())
 
-	pl, err := clientDownloadPlaylist(ctx, d.httpClient, d.onRequest, d.primaryPlaylistURL)
+	pl, err := downloadPlaylist(ctx, d.httpClient, d.onRequest, d.primaryPlaylistURL)
 	if err != nil {
 		return err
 	}
 
-	streamCount := 0
+	var streams []*clientStreamDownloader
 
 	switch plt := pl.(type) {
 	case *playlist.Media:
-		ds := &clientStreamDownloader{
+		stream := &clientStreamDownloader{
 			isLeading:                true,
 			httpClient:               d.httpClient,
 			onRequest:                d.onRequest,
@@ -177,13 +152,12 @@ func (d *clientPrimaryDownloader) run(ctx context.Context) error {
 			playlistURL:              d.primaryPlaylistURL,
 			firstPlaylist:            plt,
 			rp:                       d.rp,
-			setStreamTracks:          d.setStreamTracks,
-			setStreamEnded:           d.setStreamEnded,
 			setLeadingTimeConv:       d.setLeadingTimeConv,
 			getLeadingTimeConv:       d.getLeadingTimeConv,
 		}
-		d.rp.add(ds)
-		streamCount++
+		stream.initialize()
+		d.rp.add(stream)
+		streams = append(streams, stream)
 
 	case *playlist.Multivariant:
 		leadingPlaylist := pickLeadingPlaylist(plt.Variants)
@@ -197,7 +171,7 @@ func (d *clientPrimaryDownloader) run(ctx context.Context) error {
 			return err
 		}
 
-		ds := &clientStreamDownloader{
+		stream := &clientStreamDownloader{
 			isLeading:                true,
 			httpClient:               d.httpClient,
 			onRequest:                d.onRequest,
@@ -208,44 +182,49 @@ func (d *clientPrimaryDownloader) run(ctx context.Context) error {
 			playlistURL:              u,
 			firstPlaylist:            nil,
 			rp:                       d.rp,
-			setStreamTracks:          d.setStreamTracks,
-			setStreamEnded:           d.setStreamEnded,
 			setLeadingTimeConv:       d.setLeadingTimeConv,
 			getLeadingTimeConv:       d.getLeadingTimeConv,
 		}
-		d.rp.add(ds)
-		streamCount++
+		stream.initialize()
+		d.rp.add(stream)
+		streams = append(streams, stream)
 
 		if leadingPlaylist.Audio != "" {
-			audioPlaylist := pickAudioPlaylist(plt.Renditions, leadingPlaylist.Audio)
-			if audioPlaylist == nil {
-				return fmt.Errorf("audio playlist with id \"%s\" not found", leadingPlaylist.Audio)
+			audioPlaylists := getRenditionsByGroup(plt.Renditions, leadingPlaylist.Audio)
+			if audioPlaylists == nil {
+				return fmt.Errorf("no playlist with Group ID \"%s\" found", leadingPlaylist.Audio)
 			}
 
-			if audioPlaylist.URI != nil {
-				u, err = clientAbsoluteURL(d.primaryPlaylistURL, *audioPlaylist.URI)
-				if err != nil {
-					return err
+			for _, pl := range audioPlaylists {
+				// stream data already included in the leading playlist
+				if pl.URI == nil {
+					continue
 				}
 
-				ds := &clientStreamDownloader{
-					isLeading:                false,
-					onRequest:                d.onRequest,
-					httpClient:               d.httpClient,
-					onDownloadStreamPlaylist: d.onDownloadStreamPlaylist,
-					onDownloadSegment:        d.onDownloadSegment,
-					onDownloadPart:           d.onDownloadPart,
-					onDecodeError:            d.onDecodeError,
-					playlistURL:              u,
-					firstPlaylist:            nil,
-					rp:                       d.rp,
-					setStreamTracks:          d.setStreamTracks,
-					setLeadingTimeConv:       d.setLeadingTimeConv,
-					getLeadingTimeConv:       d.getLeadingTimeConv,
-					setStreamEnded:           d.setStreamEnded,
+				if pl.URI != nil {
+					u, err = clientAbsoluteURL(d.primaryPlaylistURL, *pl.URI)
+					if err != nil {
+						return err
+					}
+
+					stream := &clientStreamDownloader{
+						isLeading:                false,
+						onRequest:                d.onRequest,
+						httpClient:               d.httpClient,
+						onDownloadStreamPlaylist: d.onDownloadStreamPlaylist,
+						onDownloadSegment:        d.onDownloadSegment,
+						onDownloadPart:           d.onDownloadPart,
+						onDecodeError:            d.onDecodeError,
+						playlistURL:              u,
+						rendition:                pl,
+						rp:                       d.rp,
+						setLeadingTimeConv:       d.setLeadingTimeConv,
+						getLeadingTimeConv:       d.getLeadingTimeConv,
+					}
+					stream.initialize()
+					d.rp.add(stream)
+					streams = append(streams, stream)
 				}
-				d.rp.add(ds)
-				streamCount++
 			}
 		}
 
@@ -255,14 +234,10 @@ func (d *clientPrimaryDownloader) run(ctx context.Context) error {
 
 	var tracks []*Track
 
-	for i := 0; i < streamCount; i++ {
+	for _, stream := range streams {
 		select {
-		case entry := <-d.chStreamTracks:
-			if entry.isLeading {
-				tracks = append(append([]*Track(nil), entry.tracks...), tracks...)
-			} else {
-				tracks = append(tracks, entry.tracks...)
-			}
+		case streamTracks := <-stream.chTracks:
+			tracks = append(tracks, streamTracks...)
 
 		case <-ctx.Done():
 			return fmt.Errorf("terminated")
@@ -278,50 +253,21 @@ func (d *clientPrimaryDownloader) run(ctx context.Context) error {
 		return err
 	}
 
-	close(d.startStreaming)
-
-	for i := 0; i < streamCount; i++ {
+	for _, stream := range streams {
 		select {
-		case <-d.chStreamEnded:
+		case stream.chStartStreaming <- d.clientTracks:
 		case <-ctx.Done():
 			return fmt.Errorf("terminated")
 		}
 	}
 
-	return ErrClientEOS
-}
-
-func (d *clientPrimaryDownloader) setStreamTracks(
-	ctx context.Context,
-	isLeading bool,
-	tracks []*Track,
-) ([]*clientTrack, bool) {
-	select {
-	case d.chStreamTracks <- streamTracksEntry{
-		isLeading: isLeading,
-		tracks:    tracks,
-	}:
-	case <-ctx.Done():
-		return nil, false
-	}
-
-	select {
-	case <-d.startStreaming:
-	case <-ctx.Done():
-		return nil, false
-	}
-
-	streamClientTracks := make([]*clientTrack, len(tracks))
-	for i, track := range tracks {
-		streamClientTracks[i] = d.clientTracks[track]
+	for _, stream := range streams {
+		select {
+		case <-stream.chEnded:
+		case <-ctx.Done():
+			return fmt.Errorf("terminated")
+		}
 	}
 
-	return streamClientTracks, true
-}
-
-func (d *clientPrimaryDownloader) setStreamEnded(ctx context.Context) {
-	select {
-	case d.chStreamEnded <- struct{}{}:
-	case <-ctx.Done():
-	}
+	return ErrClientEOS
 }
diff --git a/client_stream_downloader.go b/client_stream_downloader.go
index b45ac02..8d0d681 100644
--- a/client_stream_downloader.go
+++ b/client_stream_downloader.go
@@ -58,15 +58,27 @@ type clientStreamDownloader struct {
 	onDownloadPart           ClientOnDownloadPartFunc
 	onDecodeError            ClientOnDecodeErrorFunc
 	playlistURL              *url.URL
+	rendition                *playlist.MultivariantRendition
 	firstPlaylist            *playlist.Media
 	rp                       *clientRoutinePool
-	setStreamTracks          clientOnStreamTracksFunc
-	setStreamEnded           func(context.Context)
 	setLeadingTimeConv       func(clientTimeConv)
 	getLeadingTimeConv       func(context.Context) (clientTimeConv, bool)
 
 	segmentQueue *clientSegmentQueue
 	curSegmentID *int
+
+	// out
+	chTracks chan []*Track
+	chEnded  chan struct{}
+
+	// in
+	chStartStreaming chan map[*Track]*clientTrack
+}
+
+func (d *clientStreamDownloader) initialize() {
+	d.chTracks = make(chan []*Track)
+	d.chEnded = make(chan struct{})
+	d.chStartStreaming = make(chan map[*Track]*clientTrack)
 }
 
 func (d *clientStreamDownloader) run(ctx context.Context) error {
@@ -82,7 +94,7 @@ func (d *clientStreamDownloader) run(ctx context.Context) error {
 	d.segmentQueue.initialize()
 
 	if d.firstPlaylist.Map != nil && d.firstPlaylist.Map.URI != "" {
-		byts, err := d.downloadSegment(
+		initFile, err := d.downloadSegment(
 			ctx,
 			d.firstPlaylist.Map.URI,
 			d.firstPlaylist.Map.ByteRangeStart,
@@ -94,11 +106,12 @@ func (d *clientStreamDownloader) run(ctx context.Context) error {
 		proc := &clientStreamProcessorFMP4{
 			ctx:                ctx,
 			isLeading:          d.isLeading,
-			initFile:           byts,
+			rendition:          d.rendition,
+			initFile:           initFile,
 			segmentQueue:       d.segmentQueue,
 			rp:                 d.rp,
-			setStreamTracks:    d.setStreamTracks,
-			setStreamEnded:     d.setStreamEnded,
+			setTracks:          d.setTracks,
+			setEnded:           d.setEnded,
 			setLeadingTimeConv: d.setLeadingTimeConv,
 			getLeadingTimeConv: d.getLeadingTimeConv,
 		}
@@ -110,8 +123,8 @@ func (d *clientStreamDownloader) run(ctx context.Context) error {
 			isLeading:          d.isLeading,
 			segmentQueue:       d.segmentQueue,
 			rp:                 d.rp,
-			setStreamTracks:    d.setStreamTracks,
-			setStreamEnded:     d.setStreamEnded,
+			setTracks:          d.setTracks,
+			setEnded:           d.setEnded,
 			setLeadingTimeConv: d.setLeadingTimeConv,
 			getLeadingTimeConv: d.getLeadingTimeConv,
 		}
@@ -190,7 +203,7 @@ func (d *clientStreamDownloader) downloadPlaylist(
 
 	d.onDownloadStreamPlaylist(ur.String())
 
-	pl, err := clientDownloadPlaylist(ctx, d.httpClient, d.onRequest, ur)
+	pl, err := downloadPlaylist(ctx, d.httpClient, d.onRequest, ur)
 	if err != nil {
 		return nil, err
 	}
@@ -346,3 +359,30 @@ func (d *clientStreamDownloader) fillSegmentQueue(
 
 	return nil
 }
+
+func (d *clientStreamDownloader) setTracks(ctx context.Context, tracks []*Track) ([]*clientTrack, bool) {
+	select {
+	case d.chTracks <- tracks:
+	case <-ctx.Done():
+		return nil, false
+	}
+
+	var allTracks map[*Track]*clientTrack
+
+	select {
+	case allTracks = <-d.chStartStreaming:
+	case <-ctx.Done():
+		return nil, false
+	}
+
+	streamTracks := make([]*clientTrack, len(tracks))
+	for i, track := range tracks {
+		streamTracks[i] = allTracks[track]
+	}
+
+	return streamTracks, true
+}
+
+func (d *clientStreamDownloader) setEnded() {
+	close(d.chEnded)
+}
diff --git a/client_stream_processor_fmp4.go b/client_stream_processor_fmp4.go
index bd7cfe6..d50b8fc 100644
--- a/client_stream_processor_fmp4.go
+++ b/client_stream_processor_fmp4.go
@@ -8,6 +8,7 @@ import (
 	"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
 
 	"github.com/bluenviron/gohlslib/v2/pkg/codecs"
+	"github.com/bluenviron/gohlslib/v2/pkg/playlist"
 )
 
 func fmp4PickLeadingTrack(init *fmp4.Init) int {
@@ -45,11 +46,12 @@ func findTimeScaleOfLeadingTrack(tracks []*fmp4.InitTrack, leadingTrackID int) u
 type clientStreamProcessorFMP4 struct {
 	ctx                context.Context
 	isLeading          bool
+	rendition          *playlist.MultivariantRendition
 	initFile           []byte
 	segmentQueue       *clientSegmentQueue
 	rp                 *clientRoutinePool
-	setStreamTracks    clientOnStreamTracksFunc
-	setStreamEnded     func(context.Context)
+	setTracks          func(ctx context.Context, tracks []*Track) ([]*clientTrack, bool)
+	setEnded           func()
 	setLeadingTimeConv func(clientTimeConv)
 	getLeadingTimeConv func(context.Context) (clientTimeConv, bool)
 
@@ -73,6 +75,10 @@ func (p *clientStreamProcessorFMP4) run(ctx context.Context) error {
 		return err
 	}
 
+	if !p.isLeading && len(p.init.Tracks) != 1 {
+		return fmt.Errorf("rendition playlists with multiple tracks are not supported")
+	}
+
 	p.leadingTrackID = fmp4PickLeadingTrack(&p.init)
 
 	tracks := make([]*Track, len(p.init.Tracks))
@@ -81,6 +87,24 @@ func (p *clientStreamProcessorFMP4) run(ctx context.Context) error {
 		tracks[i] = &Track{
 			Codec:     codecs.FromFMP4(track.Codec),
 			ClockRate: int(track.TimeScale),
+			Name: func() string {
+				if !p.isLeading {
+					return p.rendition.Name
+				}
+				return ""
+			}(),
+			Language: func() string {
+				if !p.isLeading {
+					return p.rendition.Language
+				}
+				return ""
+			}(),
+			IsDefault: func() bool {
+				if !p.isLeading {
+					return p.rendition.Default
+				}
+				return false
+			}(),
 		}
 	}
 
@@ -89,7 +113,7 @@ func (p *clientStreamProcessorFMP4) run(ctx context.Context) error {
 	}
 
 	var ok bool
-	p.clientStreamTracks, ok = p.setStreamTracks(p.ctx, p.isLeading, tracks)
+	p.clientStreamTracks, ok = p.setTracks(p.ctx, tracks)
 	if !ok {
 		return fmt.Errorf("terminated")
 	}
@@ -109,7 +133,7 @@ func (p *clientStreamProcessorFMP4) run(ctx context.Context) error {
 
 func (p *clientStreamProcessorFMP4) processSegment(ctx context.Context, seg *segmentData) error {
 	if seg == nil {
-		p.setStreamEnded(ctx)
+		p.setEnded()
 		<-ctx.Done()
 		return fmt.Errorf("terminated")
 	}
diff --git a/client_stream_processor_mpegts.go b/client_stream_processor_mpegts.go
index 7c69fdf..4fa0b22 100644
--- a/client_stream_processor_mpegts.go
+++ b/client_stream_processor_mpegts.go
@@ -39,8 +39,8 @@ type clientStreamProcessorMPEGTS struct {
 	isLeading          bool
 	segmentQueue       *clientSegmentQueue
 	rp                 *clientRoutinePool
-	setStreamTracks    clientOnStreamTracksFunc
-	setStreamEnded     func(context.Context)
+	setTracks          func(ctx context.Context, tracks []*Track) ([]*clientTrack, bool)
+	setEnded           func()
 	setLeadingTimeConv func(clientTimeConv)
 	getLeadingTimeConv func(context.Context) (clientTimeConv, bool)
 
@@ -76,7 +76,7 @@ func (p *clientStreamProcessorMPEGTS) run(ctx context.Context) error {
 
 func (p *clientStreamProcessorMPEGTS) processSegment(ctx context.Context, seg *segmentData) error {
 	if seg == nil {
-		p.setStreamEnded(ctx)
+		p.setEnded()
 		<-ctx.Done()
 		return fmt.Errorf("terminated")
 	}
@@ -174,7 +174,7 @@ func (p *clientStreamProcessorMPEGTS) initializeReader(ctx context.Context, firs
 	}
 
 	var ok bool
-	p.clientStreamTracks, ok = p.setStreamTracks(ctx, p.isLeading, tracks)
+	p.clientStreamTracks, ok = p.setTracks(ctx, tracks)
 	if !ok {
 		return fmt.Errorf("terminated")
 	}
diff --git a/client_test.go b/client_test.go
index 59e5873..f8e4d54 100644
--- a/client_test.go
+++ b/client_test.go
@@ -117,786 +117,939 @@ func mp4ToWriter(i marshaler, w io.Writer) error {
 }
 
 func TestClient(t *testing.T) {
-	for _, mode := range []string{"plain", "tls"} {
-		for _, format := range []string{"mpegts", "fmp4"} {
-			t.Run(mode+"_"+format, func(t *testing.T) {
-				httpServ := &http.Server{
-					Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-						require.Equal(t, testHeaderValue, r.Header.Get(testHeaderKey))
-
-						if format == "mpegts" {
-							switch {
-							case r.Method == http.MethodGet && r.URL.Path == "/stream.m3u8":
-								w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
-								w.Write([]byte("#EXTM3U\n" +
-									"#EXT-X-VERSION:3\n" +
-									"#EXT-X-ALLOW-CACHE:NO\n" +
-									"#EXT-X-TARGETDURATION:2\n" +
-									"#EXT-X-MEDIA-SEQUENCE:0\n" +
-									"#EXT-X-PLAYLIST-TYPE:VOD\n" +
-									"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
-									"#EXTINF:1,\n" +
-									"segment1.ts?key=val\n" +
-									"#EXTINF:1,\n" +
-									"segment2.ts\n" +
-									"#EXT-X-ENDLIST\n"))
-
-							case r.Method == http.MethodGet && r.URL.Path == "/segment1.ts":
-								q, err := url.ParseQuery(r.URL.RawQuery)
-								require.NoError(t, err)
-								require.Equal(t, "val", q.Get("key"))
-								w.Header().Set("Content-Type", `video/MP2T`)
-
-								h264Track := &mpegts.Track{
-									Codec: &mpegts.CodecH264{},
-								}
-								mpeg4audioTrack := &mpegts.Track{
-									Codec: &mpegts.CodecMPEG4Audio{
-										Config: mpeg4audio.AudioSpecificConfig{
-											Type:         2,
-											SampleRate:   44100,
-											ChannelCount: 2,
-										},
-									},
-								}
-								mw := mpegts.NewWriter(w, []*mpegts.Track{h264Track, mpeg4audioTrack})
-
-								err = mw.WriteH264(
-									h264Track,
-									90000,      // +1 sec
-									8589844592, // -1 sec
-									true,
-									[][]byte{
-										{7, 1, 2, 3}, // SPS
-										{8},          // PPS
-										{5},          // IDR
-									},
-								)
-								require.NoError(t, err)
-
-								err = mw.WriteH264(
-									h264Track,
-									90000+90000/30,
-									8589844592+90000/30,
-									false,
-									[][]byte{
-										{1, 4, 5, 6},
-									},
-								)
-								require.NoError(t, err)
-
-								err = mw.WriteMPEG4Audio(
-									mpeg4audioTrack,
-									8589844592,
-									[][]byte{{1, 2, 3, 4}},
-								)
-								require.NoError(t, err)
-
-								err = mw.WriteMPEG4Audio(
-									mpeg4audioTrack,
-									8589844592+90000/30,
-									[][]byte{{5, 6, 7, 8}},
-								)
-								require.NoError(t, err)
-
-							case r.Method == http.MethodGet && r.URL.Path == "/segment2.ts":
-								q, err := url.ParseQuery(r.URL.RawQuery)
-								require.NoError(t, err)
-								require.Equal(t, "", q.Get("key"))
-								w.Header().Set("Content-Type", `video/MP2T`)
-
-								h264Track := &mpegts.Track{
-									Codec: &mpegts.CodecH264{},
-								}
-								mpeg4audioTrack := &mpegts.Track{
-									Codec: &mpegts.CodecMPEG4Audio{
-										Config: mpeg4audio.AudioSpecificConfig{
-											Type:         2,
-											SampleRate:   44100,
-											ChannelCount: 2,
-										},
-									},
-								}
-								mw := mpegts.NewWriter(w, []*mpegts.Track{h264Track, mpeg4audioTrack})
-
-								err = mw.WriteH264(
-									h264Track,
-									8589844592+2*90000/30,
-									8589844592+2*90000/30,
-									true,
-									[][]byte{
-										{4},
-									},
-								)
-								require.NoError(t, err)
-							}
-						} else {
-							switch {
-							case r.Method == http.MethodGet && r.URL.Path == "/stream.m3u8":
-								w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
-								w.Write([]byte("#EXTM3U\n" +
-									"#EXT-X-VERSION:7\n" +
-									"#EXT-X-MEDIA-SEQUENCE:20\n" +
-									"#EXT-X-PLAYLIST-TYPE:VOD\n" +
-									"#EXT-X-INDEPENDENT-SEGMENTS\n" +
-									"#EXT-X-TARGETDURATION:2\n" +
-									"#EXT-X-MAP:URI=\"init.mp4?key=val\"\n" +
-									"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
-									"#EXTINF:2,\n" +
-									"segment1.mp4?key=val\n" +
-									"#EXTINF:2,\n" +
-									"segment2.mp4\n" +
-									"#EXT-X-ENDLIST\n"))
-
-							case r.Method == http.MethodGet && r.URL.Path == "/init.mp4":
-								q, err := url.ParseQuery(r.URL.RawQuery)
-								require.NoError(t, err)
-								require.Equal(t, "val", q.Get("key"))
-								w.Header().Set("Content-Type", `video/mp4`)
-								err = mp4ToWriter(&fmp4.Init{
-									Tracks: []*fmp4.InitTrack{
-										{
-											ID:        99,
-											TimeScale: 90000,
-											Codec: &fmp4.CodecH264{
-												SPS: testSPS,
-												PPS: testPPS,
-											},
-										},
-										{
-											ID:        98,
-											TimeScale: 44100,
-											Codec: &fmp4.CodecMPEG4Audio{
-												Config: testConfig,
-											},
-										},
-									},
-								}, w)
-								require.NoError(t, err)
-
-							case r.Method == http.MethodGet && r.URL.Path == "/segment1.mp4":
-								q, err := url.ParseQuery(r.URL.RawQuery)
-								require.NoError(t, err)
-								require.Equal(t, "val", q.Get("key"))
-								w.Header().Set("Content-Type", `video/mp4`)
-
-								err = mp4ToWriter(&fmp4.Part{
-									Tracks: []*fmp4.PartTrack{
-										{
-											ID:       98,
-											BaseTime: 44100 * 6,
-											Samples: []*fmp4.PartSample{
-												{
-													Duration: 44100 / 30,
-													Payload:  []byte{1, 2, 3, 4},
-												},
-												{
-													Duration: 44100 / 30,
-													Payload:  []byte{5, 6, 7, 8},
-												},
-											},
-										},
-										{
-											ID:       99,
-											BaseTime: 90000 * 6,
-											Samples: []*fmp4.PartSample{
-												{
-													Duration:  90000 / 30,
-													PTSOffset: 90000 * 2,
-													Payload: mustMarshalAVCC([][]byte{
-														{7, 1, 2, 3}, // SPS
-														{8},          // PPS
-														{5},          // IDR
-													}),
-												},
-												{
-													Duration:  90000 / 30,
-													PTSOffset: 90000 * 2,
-													Payload: mustMarshalAVCC([][]byte{
-														{1, 4, 5, 6},
-													}),
-												},
-											},
-										},
-									},
-								}, w)
-								require.NoError(t, err)
-
-							case r.Method == http.MethodGet && r.URL.Path == "/segment2.mp4":
-								q, err := url.ParseQuery(r.URL.RawQuery)
-								require.NoError(t, err)
-								require.Equal(t, "", q.Get("key"))
-								w.Header().Set("Content-Type", `video/mp4`)
-
-								err = mp4ToWriter(&fmp4.Part{
-									Tracks: []*fmp4.PartTrack{
-										{
-											ID:       99,
-											BaseTime: 90000*6 + 2*90000/30,
-											Samples: []*fmp4.PartSample{
-												{
-													Duration:  90000 / 30,
-													PTSOffset: 0,
-													Payload: mustMarshalAVCC([][]byte{
-														{4},
-													}),
-												},
-											},
-										},
-									},
-								}, w)
-								require.NoError(t, err)
-							}
-						}
-					}),
-				}
+	createHTTPHandler := func(t *testing.T, variant string, content string) http.HandlerFunc {
+		count := 0
 
-				ln, err := net.Listen("tcp", "localhost:5780")
-				require.NoError(t, err)
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, testHeaderValue, r.Header.Get(testHeaderKey))
 
-				if mode == "tls" {
-					go func() {
-						serverCertFpath, err2 := writeTempFile(serverCert)
-						if err2 != nil {
-							panic(err2)
-						}
-						defer os.Remove(serverCertFpath)
-
-						serverKeyFpath, err2 := writeTempFile(serverKey)
-						if err2 != nil {
-							panic(err2)
-						}
-						defer os.Remove(serverKeyFpath)
-
-						httpServ.ServeTLS(ln, serverCertFpath, serverKeyFpath)
-					}()
-				} else {
-					go httpServ.Serve(ln)
-				}
+			switch {
+			case variant == "mpegts":
+				switch {
+				case r.Method == http.MethodGet && r.URL.Path == "/index.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+					w.Write([]byte("#EXTM3U\n" +
+						"#EXT-X-VERSION:3\n" +
+						"#EXT-X-ALLOW-CACHE:NO\n" +
+						"#EXT-X-TARGETDURATION:2\n" +
+						"#EXT-X-MEDIA-SEQUENCE:0\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
+						"#EXTINF:1,\n" +
+						"segment1.ts?key=val\n" +
+						"#EXTINF:1,\n" +
+						"segment2.ts\n" +
+						"#EXT-X-ENDLIST\n"))
+
+				case r.Method == http.MethodGet && r.URL.Path == "/segment1.ts":
+					q, err := url.ParseQuery(r.URL.RawQuery)
+					require.NoError(t, err)
+					require.Equal(t, "val", q.Get("key"))
+					w.Header().Set("Content-Type", `video/MP2T`)
+
+					h264Track := &mpegts.Track{
+						Codec: &mpegts.CodecH264{},
+					}
+					mpeg4audioTrack := &mpegts.Track{
+						Codec: &mpegts.CodecMPEG4Audio{
+							Config: mpeg4audio.AudioSpecificConfig{
+								Type:         2,
+								SampleRate:   44100,
+								ChannelCount: 2,
+							},
+						},
+					}
+					mw := mpegts.NewWriter(w, []*mpegts.Track{h264Track, mpeg4audioTrack})
+
+					err = mw.WriteH264(
+						h264Track,
+						90000,      // +1 sec
+						8589844592, // -1 sec
+						true,
+						[][]byte{
+							{7, 1, 2, 3}, // SPS
+							{8},          // PPS
+							{5},          // IDR
+						},
+					)
+					require.NoError(t, err)
 
-				defer httpServ.Shutdown(context.Background())
+					err = mw.WriteH264(
+						h264Track,
+						90000+90000/30,
+						8589844592+90000/30,
+						false,
+						[][]byte{
+							{1, 4, 5, 6},
+						},
+					)
+					require.NoError(t, err)
 
-				videoRecv := make(chan struct{})
-				audioRecv := make(chan struct{})
-				videoCount := 0
-				audioCount := 0
+					err = mw.WriteMPEG4Audio(
+						mpeg4audioTrack,
+						8589844592,
+						[][]byte{{1, 2, 3, 4}},
+					)
+					require.NoError(t, err)
 
-				prefix := "http"
-				if mode == "tls" {
-					prefix = "https"
-				}
+					err = mw.WriteMPEG4Audio(
+						mpeg4audioTrack,
+						8589844592+90000/30,
+						[][]byte{{5, 6, 7, 8}},
+					)
+					require.NoError(t, err)
 
-				tr := &http.Transport{
-					TLSClientConfig: &tls.Config{
-						InsecureSkipVerify: true,
-					},
+				case r.Method == http.MethodGet && r.URL.Path == "/segment2.ts":
+					q, err := url.ParseQuery(r.URL.RawQuery)
+					require.NoError(t, err)
+					require.Equal(t, "", q.Get("key"))
+					w.Header().Set("Content-Type", `video/MP2T`)
+
+					h264Track := &mpegts.Track{
+						Codec: &mpegts.CodecH264{},
+					}
+					mpeg4audioTrack := &mpegts.Track{
+						Codec: &mpegts.CodecMPEG4Audio{
+							Config: mpeg4audio.AudioSpecificConfig{
+								Type:         2,
+								SampleRate:   44100,
+								ChannelCount: 2,
+							},
+						},
+					}
+					mw := mpegts.NewWriter(w, []*mpegts.Track{h264Track, mpeg4audioTrack})
+
+					err = mw.WriteH264(
+						h264Track,
+						8589844592+2*90000/30,
+						8589844592+2*90000/30,
+						true,
+						[][]byte{
+							{4},
+						},
+					)
+					require.NoError(t, err)
 				}
-				defer tr.CloseIdleConnections()
-
-				var c *Client
-				c = &Client{
-					URI:        prefix + "://localhost:5780/stream.m3u8",
-					HTTPClient: &http.Client{Transport: tr},
-					OnRequest: func(r *http.Request) {
-						r.Header.Set(testHeaderKey, testHeaderValue)
-					},
-					OnTracks: func(tracks []*Track) error {
-						var sps []byte
-						var pps []byte
-						if format == "fmp4" {
-							sps = testSPS
-							pps = testPPS
-						}
-
-						var audioClockRate int
-						if format == "fmp4" {
-							audioClockRate = 44100
-						} else {
-							audioClockRate = 90000
-						}
-
-						require.Equal(t, []*Track{
+
+			case variant == "fmp4_singleplaylist":
+				switch {
+				case r.Method == http.MethodGet && r.URL.Path == "/index.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+					w.Write([]byte("#EXTM3U\n" +
+						"#EXT-X-VERSION:7\n" +
+						"#EXT-X-MEDIA-SEQUENCE:20\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-INDEPENDENT-SEGMENTS\n" +
+						"#EXT-X-TARGETDURATION:2\n" +
+						"#EXT-X-MAP:URI=\"init.mp4?key=val\"\n" +
+						"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
+						"#EXTINF:2,\n" +
+						"segment1.mp4?key=val\n" +
+						"#EXTINF:2,\n" +
+						"segment2.mp4\n" +
+						"#EXT-X-ENDLIST\n"))
+
+				case r.Method == http.MethodGet && r.URL.Path == "/init.mp4":
+					q, err := url.ParseQuery(r.URL.RawQuery)
+					require.NoError(t, err)
+					require.Equal(t, "val", q.Get("key"))
+					w.Header().Set("Content-Type", `video/mp4`)
+					err = mp4ToWriter(&fmp4.Init{
+						Tracks: []*fmp4.InitTrack{
 							{
-								Codec: &codecs.H264{
-									SPS: sps,
-									PPS: pps,
+								ID:        99,
+								TimeScale: 90000,
+								Codec: &fmp4.CodecH264{
+									SPS: testSPS,
+									PPS: testPPS,
 								},
-								ClockRate: 90000,
 							},
 							{
-								Codec: &codecs.MPEG4Audio{
-									Config: mpeg4audio.AudioSpecificConfig{
-										Type:         2,
-										SampleRate:   44100,
-										ChannelCount: 2,
+								ID:        98,
+								TimeScale: 44100,
+								Codec: &fmp4.CodecMPEG4Audio{
+									Config: testConfig,
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
+
+				case r.Method == http.MethodGet && r.URL.Path == "/segment1.mp4":
+					q, err := url.ParseQuery(r.URL.RawQuery)
+					require.NoError(t, err)
+					require.Equal(t, "val", q.Get("key"))
+					w.Header().Set("Content-Type", `video/mp4`)
+
+					err = mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       98,
+								BaseTime: 44100 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{1, 2, 3, 4},
+									},
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{5, 6, 7, 8},
 									},
 								},
-								ClockRate: audioClockRate,
 							},
-						}, tracks)
-
-						c.OnDataH26x(tracks[0], func(pts int64, dts int64, au [][]byte) {
-							switch videoCount {
-							case 0:
-								require.Equal(t, int64(0), dts)
-								require.Equal(t, int64(2*90000), pts)
-								require.Equal(t, [][]byte{
-									{7, 1, 2, 3},
-									{8},
-									{5},
-								}, au)
-								ntp, ok := c.AbsoluteTime(tracks[0])
-								require.Equal(t, true, ok)
-								require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 0, time.UTC), ntp)
-
-							case 1:
-								require.Equal(t, int64(3000), dts)
-								require.Equal(t, int64(2*90000+3000), pts)
-								require.Equal(t, [][]byte{{1, 4, 5, 6}}, au)
-								ntp, ok := c.AbsoluteTime(tracks[0])
-								require.Equal(t, true, ok)
-								require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 33333333, time.UTC), ntp)
-
-							case 2:
-								require.Equal(t, int64(6000), dts)
-								require.Equal(t, int64(6000), pts)
-								require.Equal(t, [][]byte{{4}}, au)
-								ntp, ok := c.AbsoluteTime(tracks[0])
-								require.Equal(t, true, ok)
-								require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 66666666, time.UTC), ntp)
-								close(videoRecv)
-							}
-							videoCount++
-						})
-
-						c.OnDataMPEG4Audio(tracks[1], func(pts int64, aus [][]byte) {
-							switch audioCount {
-							case 0:
-								require.Equal(t, int64(0), pts)
-								require.Equal(t, [][]byte{{1, 2, 3, 4}}, aus)
-								ntp, ok := c.AbsoluteTime(tracks[1])
-								require.Equal(t, true, ok)
-								require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 0, time.UTC), ntp)
-
-							case 1:
-								require.Equal(t, int64(0.0333336*float64(tracks[1].ClockRate)), pts)
-								require.Equal(t, [][]byte{{5, 6, 7, 8}}, aus)
-								ntp, ok := c.AbsoluteTime(tracks[1])
-								require.Equal(t, true, ok)
-								require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 33333333, time.UTC), ntp)
-								close(audioRecv)
-							}
-							audioCount++
-						})
+							{
+								ID:       99,
+								BaseTime: 90000 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{7, 1, 2, 3}, // SPS
+											{8},          // PPS
+											{5},          // IDR
+										}),
+									},
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{1, 4, 5, 6},
+										}),
+									},
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
 
-						return nil
-					},
-				}
+				case r.Method == http.MethodGet && r.URL.Path == "/segment2.mp4":
+					q, err := url.ParseQuery(r.URL.RawQuery)
+					require.NoError(t, err)
+					require.Equal(t, "", q.Get("key"))
+					w.Header().Set("Content-Type", `video/mp4`)
 
-				err = c.Start()
-				require.NoError(t, err)
+					err = mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       99,
+								BaseTime: 90000*6 + 2*90000/30,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 0,
+										Payload: mustMarshalAVCC([][]byte{
+											{4},
+										}),
+									},
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
+				}
 
-				<-videoRecv
-				<-audioRecv
+			case variant == "fmp4_multiplaylist" && content == "video+audio":
+				switch {
+				case r.Method == http.MethodGet && r.URL.Path == "/index.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+					w.Write([]byte("#EXTM3U\n" +
+						"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\"," +
+						"DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"audio.m3u8\"\n" +
+						"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640015,mp4a.40.5\",AUDIO=\"aac\"\n" +
+						"video.m3u8\n"))
 
-				c.Close()
-			})
-		}
-	}
-}
+				case r.Method == http.MethodGet && r.URL.Path == "/video.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+					w.Write([]byte("#EXTM3U\n" +
+						"#EXT-X-VERSION:7\n" +
+						"#EXT-X-MEDIA-SEQUENCE:20\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-INDEPENDENT-SEGMENTS\n" +
+						"#EXT-X-TARGETDURATION:2\n" +
+						"#EXT-X-MAP:URI=\"init_video.mp4\"\n" +
+						"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
+						"#EXTINF:2,\n" +
+						"segment_video.mp4\n" +
+						"#EXT-X-ENDLIST\n"))
 
-func TestClientFMP4MultiRenditions(t *testing.T) {
-	httpServ := &http.Server{
-		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			switch {
-			case r.Method == http.MethodGet && r.URL.Path == "/index.m3u8":
-				w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
-				w.Write([]byte("#EXTM3U\n" +
-					"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\"," +
-					"DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"audio.m3u8\"\n" +
-					"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640015,mp4a.40.5\",AUDIO=\"aac\"\n" +
-					"video.m3u8\n"))
+				case r.Method == http.MethodGet && r.URL.Path == "/audio.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+					w.Write([]byte("#EXTM3U\n" +
+						"#EXT-X-VERSION:7\n" +
+						"#EXT-X-MEDIA-SEQUENCE:20\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-INDEPENDENT-SEGMENTS\n" +
+						"#EXT-X-TARGETDURATION:2\n" +
+						"#EXT-X-MAP:URI=\"init_audio.mp4\"\n" +
+						"#EXT-X-PROGRAM-DATE-TIME:2014-02-05T01:02:02Z\n" +
+						"#EXTINF:2,\n" +
+						"segment_audio.mp4\n" +
+						"#EXT-X-ENDLIST"))
 
-			case r.Method == http.MethodGet && r.URL.Path == "/video.m3u8":
-				w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
-				w.Write([]byte("#EXTM3U\n" +
-					"#EXT-X-VERSION:7\n" +
-					"#EXT-X-MEDIA-SEQUENCE:20\n" +
-					"#EXT-X-PLAYLIST-TYPE:VOD\n" +
-					"#EXT-X-INDEPENDENT-SEGMENTS\n" +
-					"#EXT-X-TARGETDURATION:2\n" +
-					"#EXT-X-MAP:URI=\"init_video.mp4\"\n" +
-					"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
-					"#EXTINF:2,\n" +
-					"segment_video.mp4\n" +
-					"#EXT-X-ENDLIST\n"))
-
-			case r.Method == http.MethodGet && r.URL.Path == "/audio.m3u8":
-				w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
-				w.Write([]byte("#EXTM3U\n" +
-					"#EXT-X-VERSION:7\n" +
-					"#EXT-X-MEDIA-SEQUENCE:20\n" +
-					"#EXT-X-PLAYLIST-TYPE:VOD\n" +
-					"#EXT-X-INDEPENDENT-SEGMENTS\n" +
-					"#EXT-X-TARGETDURATION:2\n" +
-					"#EXT-X-MAP:URI=\"init_audio.mp4\"\n" +
-					"#EXT-X-PROGRAM-DATE-TIME:2014-02-05T01:02:02Z\n" +
-					"#EXTINF:2,\n" +
-					"segment_audio.mp4\n" +
-					"#EXT-X-ENDLIST"))
-
-			case r.Method == http.MethodGet && r.URL.Path == "/init_video.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Init{
-					Tracks: []*fmp4.InitTrack{
-						{
-							ID:        1,
-							TimeScale: 90000,
-							Codec: &fmp4.CodecH264{
-								SPS: testSPS,
-								PPS: testPPS,
+				case r.Method == http.MethodGet && r.URL.Path == "/init_video.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Init{
+						Tracks: []*fmp4.InitTrack{
+							{
+								ID:        1,
+								TimeScale: 90000,
+								Codec: &fmp4.CodecH264{
+									SPS: testSPS,
+									PPS: testPPS,
+								},
 							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
+					}, w)
+					require.NoError(t, err)
 
-			case r.Method == http.MethodGet && r.URL.Path == "/init_audio.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Init{
-					Tracks: []*fmp4.InitTrack{
-						{
-							ID:        1,
-							TimeScale: 44100,
-							Codec: &fmp4.CodecMPEG4Audio{
-								Config: testConfig,
+				case r.Method == http.MethodGet && r.URL.Path == "/init_audio.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Init{
+						Tracks: []*fmp4.InitTrack{
+							{
+								ID:        1,
+								TimeScale: 44100,
+								Codec: &fmp4.CodecMPEG4Audio{
+									Config: testConfig,
+								},
 							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
+					}, w)
+					require.NoError(t, err)
 
-			case r.Method == http.MethodGet && r.URL.Path == "/segment_video.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Part{
-					Tracks: []*fmp4.PartTrack{
-						{
-							ID: 1,
-							Samples: []*fmp4.PartSample{{
-								Duration:  90000,
-								PTSOffset: 90000 * 3,
-								Payload: mustMarshalAVCC([][]byte{
-									{7, 1, 2, 3}, // SPS
-									{8},          // PPS
-									{5},          // IDR
-								}),
-							}},
+				case r.Method == http.MethodGet && r.URL.Path == "/segment_video.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       1,
+								BaseTime: 90000 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{7, 1, 2, 3}, // SPS
+											{8},          // PPS
+											{5},          // IDR
+										}),
+									},
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{1, 4, 5, 6},
+										}),
+									},
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 0,
+										Payload: mustMarshalAVCC([][]byte{
+											{4},
+										}),
+									},
+								},
+							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
+					}, w)
+					require.NoError(t, err)
 
-			case r.Method == http.MethodGet && r.URL.Path == "/segment_audio.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Part{
-					Tracks: []*fmp4.PartTrack{
-						{
-							ID:       1,
-							BaseTime: 44100 / 2, // +0.5 sec
-							Samples: []*fmp4.PartSample{{
-								Duration: 44100,
-								Payload:  []byte{1, 2, 3, 4},
-							}},
+				case r.Method == http.MethodGet && r.URL.Path == "/segment_audio.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       1,
+								BaseTime: 44100 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{1, 2, 3, 4},
+									},
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{5, 6, 7, 8},
+									},
+								},
+							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
-			}
-		}),
-	}
-
-	ln, err := net.Listen("tcp", "localhost:5780")
-	require.NoError(t, err)
-
-	go httpServ.Serve(ln)
-	defer httpServ.Shutdown(context.Background())
-
-	packetRecv := make(chan struct{}, 2)
-	tracksRecv := make(chan struct{}, 1)
-
-	tr := &http.Transport{}
-	defer tr.CloseIdleConnections()
-
-	var c *Client
-	c = &Client{
-		URI:        "http://localhost:5780/index.m3u8",
-		HTTPClient: &http.Client{Transport: tr},
-		OnTracks: func(tracks []*Track) error {
-			close(tracksRecv)
-
-			require.Equal(t, []*Track{
-				{
-					Codec: &codecs.H264{
-						SPS: testSPS,
-						PPS: testPPS,
-					},
-					ClockRate: 90000,
-				},
-				{
-					Codec: &codecs.MPEG4Audio{
-						Config: testConfig,
-					},
-					ClockRate: 44100,
-				},
-			}, tracks)
-
-			c.OnDataH26x(tracks[0], func(pts int64, dts int64, au [][]byte) {
-				require.Equal(t, int64(3*90000), pts)
-				require.Equal(t, int64(0), dts)
-				require.Equal(t, [][]byte{
-					{7, 1, 2, 3},
-					{8},
-					{5},
-				}, au)
-				ntp, ok := c.AbsoluteTime(tracks[0])
-				require.Equal(t, true, ok)
-				require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 0, time.UTC), ntp)
-				packetRecv <- struct{}{}
-			})
-
-			c.OnDataMPEG4Audio(tracks[1], func(pts int64, aus [][]byte) {
-				require.Equal(t, int64(22050), pts)
-				require.Equal(t, [][]byte{
-					{1, 2, 3, 4},
-				}, aus)
-				ntp, ok := c.AbsoluteTime(tracks[1])
-				require.Equal(t, true, ok)
-				require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 500000000, time.UTC), ntp)
-				packetRecv <- struct{}{}
-			})
-
-			return nil
-		},
-	}
-
-	err = c.Start()
-	require.NoError(t, err)
-
-	for i := 0; i < 2; i++ {
-		<-packetRecv
-	}
-
-	<-c.Wait()
-	c.Close()
-}
-
-func TestClientFMP4LowLatency(t *testing.T) {
-	count := 0
-	closeRequest := make(chan struct{})
-
-	httpServ := &http.Server{
-		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			switch {
-			case r.Method == http.MethodGet && r.URL.Path == "/stream.m3u8":
-				w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
-
-				switch count {
-				case 0:
-					q, err := url.ParseQuery(r.URL.RawQuery)
+					}, w)
 					require.NoError(t, err)
-					require.Equal(t, "", q.Get("_HLS_skip"))
+				}
+
+			case variant == "fmp4_multiplaylist" && content == "video+multiaudio":
+				switch {
+				case r.Method == http.MethodGet && r.URL.Path == "/index.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+					w.Write([]byte("#EXTM3U\n" +
+						"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\"," +
+						"AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"audio1.m3u8\"\n" +
+						"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"German\"," +
+						"DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"de\",URI=\"audio2.m3u8\"\n" +
+						"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640015,mp4a.40.5\",AUDIO=\"aac\"\n" +
+						"video.m3u8\n"))
+
+				case r.Method == http.MethodGet && r.URL.Path == "/video.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
 					w.Write([]byte("#EXTM3U\n" +
-						"#EXT-X-VERSION:9\n" +
-						"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24.00000\n" +
+						"#EXT-X-VERSION:7\n" +
 						"#EXT-X-MEDIA-SEQUENCE:20\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-INDEPENDENT-SEGMENTS\n" +
 						"#EXT-X-TARGETDURATION:2\n" +
-						"#EXT-X-MAP:URI=\"init.mp4\"\n" +
+						"#EXT-X-MAP:URI=\"init_video.mp4\"\n" +
 						"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
 						"#EXTINF:2,\n" +
-						"segment.mp4\n" +
-						"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=part1.mp4\n"))
+						"segment_video.mp4\n" +
+						"#EXT-X-ENDLIST\n"))
 
-				case 1:
-					q, err := url.ParseQuery(r.URL.RawQuery)
-					require.NoError(t, err)
-					require.Equal(t, "YES", q.Get("_HLS_skip"))
+				case r.Method == http.MethodGet && r.URL.Path == "/audio1.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
 					w.Write([]byte("#EXTM3U\n" +
-						"#EXT-X-VERSION:9\n" +
-						"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24.00000\n" +
+						"#EXT-X-VERSION:7\n" +
 						"#EXT-X-MEDIA-SEQUENCE:20\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-INDEPENDENT-SEGMENTS\n" +
 						"#EXT-X-TARGETDURATION:2\n" +
-						"#EXT-X-MAP:URI=\"init.mp4\"\n" +
-						"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
+						"#EXT-X-MAP:URI=\"init_audio1.mp4\"\n" +
+						"#EXT-X-PROGRAM-DATE-TIME:2014-02-05T01:02:02Z\n" +
 						"#EXTINF:2,\n" +
-						"segment.mp4\n" +
-						"#EXT-X-PART:DURATION=0.066666666,URI=\"part1.mp4\",INDEPENDENT=YES\n" +
-						"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=part2.mp4\n"))
+						"segment_audio1.mp4\n" +
+						"#EXT-X-ENDLIST"))
 
-				case 2:
-					q, err := url.ParseQuery(r.URL.RawQuery)
-					require.NoError(t, err)
-					require.Equal(t, "YES", q.Get("_HLS_skip"))
+				case r.Method == http.MethodGet && r.URL.Path == "/audio2.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
 					w.Write([]byte("#EXTM3U\n" +
-						"#EXT-X-VERSION:9\n" +
-						"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24.00000\n" +
+						"#EXT-X-VERSION:7\n" +
 						"#EXT-X-MEDIA-SEQUENCE:20\n" +
+						"#EXT-X-PLAYLIST-TYPE:VOD\n" +
+						"#EXT-X-INDEPENDENT-SEGMENTS\n" +
 						"#EXT-X-TARGETDURATION:2\n" +
-						"#EXT-X-MAP:URI=\"init.mp4\"\n" +
-						"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:02Z\n" +
+						"#EXT-X-MAP:URI=\"init_audio2.mp4\"\n" +
+						"#EXT-X-PROGRAM-DATE-TIME:2014-02-05T01:02:02Z\n" +
 						"#EXTINF:2,\n" +
-						"segment.mp4\n" +
-						"#EXT-X-PART:DURATION=0.066666666,URI=\"part1.mp4\",INDEPENDENT=YES\n" +
-						"#EXT-X-PART:DURATION=0.033333333,URI=\"part2.mp4\"\n" +
-						"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=part3.mp4\n"))
+						"segment_audio2.mp4\n" +
+						"#EXT-X-ENDLIST"))
+
+				case r.Method == http.MethodGet && r.URL.Path == "/init_video.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Init{
+						Tracks: []*fmp4.InitTrack{
+							{
+								ID:        1,
+								TimeScale: 90000,
+								Codec: &fmp4.CodecH264{
+									SPS: testSPS,
+									PPS: testPPS,
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
+
+				case r.Method == http.MethodGet && (r.URL.Path == "/init_audio1.mp4" || r.URL.Path == "/init_audio2.mp4"):
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Init{
+						Tracks: []*fmp4.InitTrack{
+							{
+								ID:        1,
+								TimeScale: 44100,
+								Codec: &fmp4.CodecMPEG4Audio{
+									Config: testConfig,
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
+
+				case r.Method == http.MethodGet && r.URL.Path == "/segment_video.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       1,
+								BaseTime: 90000 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{7, 1, 2, 3}, // SPS
+											{8},          // PPS
+											{5},          // IDR
+										}),
+									},
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{1, 4, 5, 6},
+										}),
+									},
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 0,
+										Payload: mustMarshalAVCC([][]byte{
+											{4},
+										}),
+									},
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
+
+				case r.Method == http.MethodGet && r.URL.Path == "/segment_audio1.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       1,
+								BaseTime: 44100 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{1, 2, 3, 4},
+									},
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{5, 6, 7, 8},
+									},
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
+
+				case r.Method == http.MethodGet && r.URL.Path == "/segment_audio2.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       1,
+								BaseTime: 44100 * 6,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{4, 3, 2, 1},
+									},
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{8, 7, 5, 4},
+									},
+								},
+							},
+						},
+					}, w)
+					require.NoError(t, err)
 				}
-				count++
-
-			case r.Method == http.MethodGet && r.URL.Path == "/init.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Init{
-					Tracks: []*fmp4.InitTrack{
-						{
-							ID:        1,
-							TimeScale: 90000,
-							Codec: &fmp4.CodecH264{
-								SPS: testSPS,
-								PPS: testPPS,
+
+			case variant == "lowlatency_singleplaylist":
+				switch {
+				case r.Method == http.MethodGet && r.URL.Path == "/index.m3u8":
+					w.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
+
+					switch count {
+					case 0:
+						q, err := url.ParseQuery(r.URL.RawQuery)
+						require.NoError(t, err)
+						require.Equal(t, "", q.Get("_HLS_skip"))
+						w.Write([]byte("#EXTM3U\n" +
+							"#EXT-X-VERSION:9\n" +
+							"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24.00000\n" +
+							"#EXT-X-MEDIA-SEQUENCE:20\n" +
+							"#EXT-X-TARGETDURATION:2\n" +
+							"#EXT-X-MAP:URI=\"init.mp4\"\n" +
+							"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:00Z\n" +
+							"#EXTINF:2,\n" +
+							"segment.mp4\n" +
+							"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=part1.mp4\n"))
+
+					case 1:
+						q, err := url.ParseQuery(r.URL.RawQuery)
+						require.NoError(t, err)
+						require.Equal(t, "YES", q.Get("_HLS_skip"))
+						w.Write([]byte("#EXTM3U\n" +
+							"#EXT-X-VERSION:9\n" +
+							"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24.00000\n" +
+							"#EXT-X-MEDIA-SEQUENCE:20\n" +
+							"#EXT-X-TARGETDURATION:2\n" +
+							"#EXT-X-MAP:URI=\"init.mp4\"\n" +
+							"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:00Z\n" +
+							"#EXTINF:2,\n" +
+							"segment.mp4\n" +
+							"#EXT-X-PART:DURATION=0.066666666,URI=\"part1.mp4\",INDEPENDENT=YES\n" +
+							"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=part2.mp4\n"))
+
+					case 2:
+						q, err := url.ParseQuery(r.URL.RawQuery)
+						require.NoError(t, err)
+						require.Equal(t, "YES", q.Get("_HLS_skip"))
+						w.Write([]byte("#EXTM3U\n" +
+							"#EXT-X-VERSION:9\n" +
+							"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24.00000\n" +
+							"#EXT-X-MEDIA-SEQUENCE:20\n" +
+							"#EXT-X-TARGETDURATION:2\n" +
+							"#EXT-X-MAP:URI=\"init.mp4\"\n" +
+							"#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:00Z\n" +
+							"#EXTINF:2,\n" +
+							"segment.mp4\n" +
+							"#EXT-X-PART:DURATION=0.066666666,URI=\"part1.mp4\",INDEPENDENT=YES\n" +
+							"#EXT-X-PART:DURATION=0.033333333,URI=\"part2.mp4\"\n" +
+							"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=part3.mp4\n"))
+					}
+					count++
+
+				case r.Method == http.MethodGet && r.URL.Path == "/init.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Init{
+						Tracks: []*fmp4.InitTrack{
+							{
+								ID:        1,
+								TimeScale: 90000,
+								Codec: &fmp4.CodecH264{
+									SPS: testSPS,
+									PPS: testPPS,
+								},
+							},
+							{
+								ID:        2,
+								TimeScale: 44100,
+								Codec: &fmp4.CodecMPEG4Audio{
+									Config: testConfig,
+								},
 							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
+					}, w)
+					require.NoError(t, err)
 
-			case r.Method == http.MethodGet && r.URL.Path == "/part1.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Part{
-					Tracks: []*fmp4.PartTrack{
-						{
-							ID: 1,
-							Samples: []*fmp4.PartSample{
-								{
-									Duration: 90000 / 30,
-									Payload: mustMarshalAVCC([][]byte{
-										{7, 1, 2, 3}, // SPS
-										{8},          // PPS
-										{5},          // IDR
-									}),
+				case r.Method == http.MethodGet && r.URL.Path == "/part1.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID: 1,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{7, 1, 2, 3}, // SPS
+											{8},          // PPS
+											{5},          // IDR
+										}),
+									},
+									{
+										Duration:  90000 / 30,
+										PTSOffset: 90000 * 2,
+										Payload: mustMarshalAVCC([][]byte{
+											{1, 4, 5, 6},
+										}),
+									},
 								},
-								{
-									Duration: 90000 / 30,
-									Payload: mustMarshalAVCC([][]byte{
-										{1, 4, 5, 6},
-									}),
+							},
+							{
+								ID: 2,
+								Samples: []*fmp4.PartSample{
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{1, 2, 3, 4},
+									},
+									{
+										Duration: 44100 / 30,
+										Payload:  []byte{5, 6, 7, 8},
+									},
 								},
 							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
+					}, w)
+					require.NoError(t, err)
 
-			case r.Method == http.MethodGet && r.URL.Path == "/part2.mp4":
-				w.Header().Set("Content-Type", `video/mp4`)
-				err := mp4ToWriter(&fmp4.Part{
-					Tracks: []*fmp4.PartTrack{
-						{
-							ID:       1,
-							BaseTime: (90000 / 30) * 2,
-							Samples: []*fmp4.PartSample{{
-								Duration: 90000 / 30,
-								Payload: mustMarshalAVCC([][]byte{
-									{1, 7, 8, 9},
-								}),
-							}},
+				case r.Method == http.MethodGet && r.URL.Path == "/part2.mp4":
+					w.Header().Set("Content-Type", `video/mp4`)
+					err := mp4ToWriter(&fmp4.Part{
+						Tracks: []*fmp4.PartTrack{
+							{
+								ID:       1,
+								BaseTime: (90000 / 30) * 2,
+								Samples: []*fmp4.PartSample{{
+									Duration: 90000 / 30,
+									Payload: mustMarshalAVCC([][]byte{
+										{4},
+									}),
+								}},
+							},
 						},
-					},
-				}, w)
-				require.NoError(t, err)
+					}, w)
+					require.NoError(t, err)
 
-			case r.Method == http.MethodGet && r.URL.Path == "/part3.mp4":
-				<-closeRequest
+				case r.Method == http.MethodGet && r.URL.Path == "/part3.mp4":
+					time.Sleep(1 * time.Second) // wait until client closes
+				}
 			}
-		}),
+		})
 	}
 
-	ln, err := net.Listen("tcp", "localhost:5780")
-	require.NoError(t, err)
-
-	go httpServ.Serve(ln)
-	defer httpServ.Shutdown(context.Background())
+	createHTTPServer := func(t *testing.T, encryption string, variant string, content string) *http.Server {
+		httpServ := &http.Server{
+			Handler: createHTTPHandler(t, variant, content),
+		}
 
-	packetRecv := make(chan struct{})
-	recvCount := 0
+		ln, err := net.Listen("tcp", "localhost:5780")
+		require.NoError(t, err)
 
-	tr := &http.Transport{}
-	defer tr.CloseIdleConnections()
+		if encryption == "tls" {
+			go func() {
+				serverCertFpath, err2 := writeTempFile(serverCert)
+				if err2 != nil {
+					panic(err2)
+				}
+				defer os.Remove(serverCertFpath)
 
-	var c *Client
-	c = &Client{
-		URI:        "http://localhost:5780/stream.m3u8",
-		HTTPClient: &http.Client{Transport: tr},
-		OnTracks: func(tracks []*Track) error {
-			require.Equal(t, []*Track{
-				{
-					Codec: &codecs.H264{
-						SPS: testSPS,
-						PPS: testPPS,
-					},
-					ClockRate: 90000,
-				},
-			}, tracks)
-
-			c.OnDataH26x(tracks[0], func(pts int64, dts int64, au [][]byte) {
-				switch recvCount {
-				case 0:
-					ntp, ok := c.AbsoluteTime(tracks[0])
-					require.Equal(t, true, ok)
-					require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 4, 0, time.UTC), ntp)
-					require.Equal(t, int64(0), pts)
-					require.Equal(t, int64(0), dts)
-					require.Equal(t, [][]byte{
-						{7, 1, 2, 3},
-						{8},
-						{5},
-					}, au)
-
-				case 1:
-					ntp, ok := c.AbsoluteTime(tracks[0])
-					require.Equal(t, true, ok)
-					require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 4, 33333333, time.UTC), ntp)
-					require.Equal(t, int64(3000), pts)
-					require.Equal(t, int64(3000), dts)
-					require.Equal(t, [][]byte{{1, 4, 5, 6}}, au)
-
-				case 2:
-					ntp, ok := c.AbsoluteTime(tracks[0])
-					require.Equal(t, true, ok)
-					require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 4, 66666666, time.UTC), ntp)
-					require.Equal(t, int64(6000), pts)
-					require.Equal(t, int64(6000), dts)
-					require.Equal(t, [][]byte{{1, 7, 8, 9}}, au)
-
-				default:
-					t.Errorf("should not happen")
+				serverKeyFpath, err2 := writeTempFile(serverKey)
+				if err2 != nil {
+					panic(err2)
 				}
-				recvCount++
-				packetRecv <- struct{}{}
-			})
+				defer os.Remove(serverKeyFpath)
 
-			return nil
-		},
+				httpServ.ServeTLS(ln, serverCertFpath, serverKeyFpath)
+			}()
+		} else {
+			go httpServ.Serve(ln)
+		}
+
+		return httpServ
 	}
 
-	err = c.Start()
-	require.NoError(t, err)
+	for _, encryption := range []string{"plain", "tls"} {
+		for _, variant := range []string{
+			"mpegts",
+			"fmp4_singleplaylist",
+			"fmp4_multiplaylist",
+			"lowlatency_singleplaylist",
+		} {
+			for _, content := range []string{
+				"video+audio",
+				"video+multiaudio",
+			} {
+				if content == "video+multiaudio" && variant != "fmp4_multiplaylist" {
+					continue
+				}
+				t.Run(encryption+"_"+variant+"_"+content, func(t *testing.T) {
+					httpServ := createHTTPServer(t, encryption, variant, content)
+					defer httpServ.Shutdown(context.Background())
+
+					videoRecv := make(chan struct{})
+					audioRecv := make(chan struct{})
+					audio2Recv := make(chan struct{})
+
+					videoCount := 0
+					audioCount := 0
+					audio2Count := 0
+
+					prefix := "http"
+					if encryption == "tls" {
+						prefix = "https"
+					}
+
+					tr := &http.Transport{
+						TLSClientConfig: &tls.Config{
+							InsecureSkipVerify: true,
+						},
+					}
+					defer tr.CloseIdleConnections()
+
+					var c *Client
+					c = &Client{
+						URI:        prefix + "://localhost:5780/index.m3u8",
+						HTTPClient: &http.Client{Transport: tr},
+						OnRequest: func(r *http.Request) {
+							r.Header.Set(testHeaderKey, testHeaderValue)
+						},
+						OnTracks: func(tracks []*Track) error {
+							var sps []byte
+							var pps []byte
+							if variant != "mpegts" {
+								sps = testSPS
+								pps = testPPS
+							}
 
-	for i := 0; i < 3; i++ {
-		<-packetRecv
-	}
+							var audioClockRate int
+							if variant != "mpegts" {
+								audioClockRate = 44100
+							} else {
+								audioClockRate = 90000
+							}
 
-	c.Close()
-	<-c.Wait()
+							switch content {
+							case "video+audio":
+								require.Equal(t, []*Track{
+									{
+										Codec: &codecs.H264{
+											SPS: sps,
+											PPS: pps,
+										},
+										ClockRate: 90000,
+									},
+									{
+										Codec: &codecs.MPEG4Audio{
+											Config: mpeg4audio.AudioSpecificConfig{
+												Type:         2,
+												SampleRate:   44100,
+												ChannelCount: 2,
+											},
+										},
+										ClockRate: audioClockRate,
+										Name: func() string {
+											if variant == "fmp4_multiplaylist" {
+												return "English"
+											}
+											return ""
+										}(),
+										Language: func() string {
+											if variant == "fmp4_multiplaylist" {
+												return "en"
+											}
+											return ""
+										}(),
+										IsDefault: (variant == "fmp4_multiplaylist"),
+									},
+								}, tracks)
+
+							case "video+multiaudio":
+								require.Equal(t, []*Track{
+									{
+										Codec: &codecs.H264{
+											SPS: sps,
+											PPS: pps,
+										},
+										ClockRate: 90000,
+									},
+									{
+										Codec: &codecs.MPEG4Audio{
+											Config: mpeg4audio.AudioSpecificConfig{
+												Type:         2,
+												SampleRate:   44100,
+												ChannelCount: 2,
+											},
+										},
+										ClockRate: audioClockRate,
+										Name:      "English",
+										Language:  "en",
+										IsDefault: false,
+									},
+									{
+										Codec: &codecs.MPEG4Audio{
+											Config: mpeg4audio.AudioSpecificConfig{
+												Type:         2,
+												SampleRate:   44100,
+												ChannelCount: 2,
+											},
+										},
+										ClockRate: audioClockRate,
+										Name:      "German",
+										Language:  "de",
+										IsDefault: true,
+									},
+								}, tracks)
+							}
+
+							c.OnDataH26x(tracks[0], func(pts int64, dts int64, au [][]byte) {
+								switch videoCount {
+								case 0:
+									require.Equal(t, int64(0), dts)
+									require.Equal(t, int64(2*90000), pts)
+									require.Equal(t, [][]byte{
+										{7, 1, 2, 3},
+										{8},
+										{5},
+									}, au)
+									ntp, ok := c.AbsoluteTime(tracks[0])
+									require.Equal(t, true, ok)
+									require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 0, time.UTC), ntp)
+
+								case 1:
+									require.Equal(t, int64(3000), dts)
+									require.Equal(t, int64(2*90000+3000), pts)
+									require.Equal(t, [][]byte{{1, 4, 5, 6}}, au)
+									ntp, ok := c.AbsoluteTime(tracks[0])
+									require.Equal(t, true, ok)
+									require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 33333333, time.UTC), ntp)
+
+								case 2:
+									require.Equal(t, int64(6000), dts)
+									require.Equal(t, int64(6000), pts)
+									require.Equal(t, [][]byte{{4}}, au)
+									ntp, ok := c.AbsoluteTime(tracks[0])
+									require.Equal(t, true, ok)
+									require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 66666666, time.UTC), ntp)
+									close(videoRecv)
+								}
+								videoCount++
+							})
+
+							c.OnDataMPEG4Audio(tracks[1], func(pts int64, aus [][]byte) { //nolint:dupl
+								switch audioCount {
+								case 0:
+									require.Equal(t, int64(0), pts)
+									require.Equal(t, [][]byte{{1, 2, 3, 4}}, aus)
+									ntp, ok := c.AbsoluteTime(tracks[1])
+									require.Equal(t, true, ok)
+									require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 0, time.UTC), ntp)
+
+								case 1:
+									require.Equal(t, int64(0.0333336*float64(tracks[1].ClockRate)), pts)
+									require.Equal(t, [][]byte{{5, 6, 7, 8}}, aus)
+									ntp, ok := c.AbsoluteTime(tracks[1])
+									require.Equal(t, true, ok)
+									require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 33333333, time.UTC), ntp)
+									close(audioRecv)
+								}
+								audioCount++
+							})
+
+							if content == "video+multiaudio" {
+								c.OnDataMPEG4Audio(tracks[2], func(pts int64, aus [][]byte) { //nolint:dupl
+									switch audio2Count {
+									case 0:
+										require.Equal(t, int64(0), pts)
+										require.Equal(t, [][]byte{{4, 3, 2, 1}}, aus)
+										ntp, ok := c.AbsoluteTime(tracks[2])
+										require.Equal(t, true, ok)
+										require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 0, time.UTC), ntp)
+
+									case 1:
+										require.Equal(t, int64(0.0333336*float64(tracks[1].ClockRate)), pts)
+										require.Equal(t, [][]byte{{8, 7, 5, 4}}, aus)
+										ntp, ok := c.AbsoluteTime(tracks[2])
+										require.Equal(t, true, ok)
+										require.Equal(t, time.Date(2015, time.February, 5, 1, 2, 2, 33333333, time.UTC), ntp)
+										close(audio2Recv)
+									}
+									audio2Count++
+								})
+							}
 
-	close(closeRequest)
+							return nil
+						},
+					}
+
+					err := c.Start()
+					require.NoError(t, err)
+
+					<-videoRecv
+					<-audioRecv
+
+					if content == "video+multiaudio" {
+						<-audio2Recv
+					}
+
+					c.Close()
+				})
+			}
+		}
+	}
 }
 
 func TestClientErrorInvalidSequenceID(t *testing.T) {
diff --git a/client_time_conv_mpegts.go b/client_time_conv_mpegts.go
index 1f173f8..9162c68 100644
--- a/client_time_conv_mpegts.go
+++ b/client_time_conv_mpegts.go
@@ -47,5 +47,6 @@ func (ts *clientTimeConvMPEGTS) getNTP(timestamp int64) *time.Time {
 	}
 
 	v := ts.ntpValue.Add(timestampToDuration(timestamp-ts.ntpTimestamp, 90000))
+
 	return &v
 }
diff --git a/client_track.go b/client_track.go
index 43ed896..9c1371b 100644
--- a/client_track.go
+++ b/client_track.go
@@ -8,7 +8,7 @@ import (
 
 type clientTrack struct {
 	track            *Track
-	onData           clientOnDataFunc
+	onData           func(pts int64, dts int64, data [][]byte)
 	lastAbsoluteTime *time.Time
 	startRTC         time.Time
 }
diff --git a/muxer_test.go b/muxer_test.go
index ba81f78..7c99897 100644
--- a/muxer_test.go
+++ b/muxer_test.go
@@ -57,8 +57,6 @@ var testAudioTrack = &Track{
 }
 
 var testAudioTrack2 = &Track{
-	Name:     "German",
-	Language: "de",
 	Codec: &codecs.MPEG4Audio{
 		Config: mpeg4audio.Config{
 			Type:         2,
@@ -67,6 +65,8 @@ var testAudioTrack2 = &Track{
 		},
 	},
 	ClockRate: 44100,
+	Name:      "German",
+	Language:  "de",
 }
 
 type dummyResponseWriter struct {