Skip to content

Streaming API

The streaming module provides audio playback capabilities (placeholder).

MusicPlayer

qfzz.streaming.player.MusicPlayer

Music player for streaming playback.

Manages a local streaming server and playlist state.

Source code in qfzz/streaming/player.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
class MusicPlayer:
    """
    Music player for streaming playback.

    Manages a local streaming server and playlist state.
    """

    def __init__(self, content_dir: str = "./audio_content", port: int = 8000):
        """
        Initialize music player.

        Args:
            content_dir: Directory to serve audio from
            port: Streaming port
        """
        self._state = PlayerState.STOPPED
        self._current_track: Optional[dict[str, Any]] = None
        self._playlist: list[dict[str, Any]] = []
        self._current_index = -1
        self._volume = 0.8
        self._position_seconds = 0
        self._playback_history: list[dict[str, Any]] = []

        # Setup content directory
        self.content_dir = os.path.abspath(content_dir)
        os.makedirs(self.content_dir, exist_ok=True)

        # Initialize streaming server
        self.server = StreamingServer(self.content_dir, port)
        self.server.start()

        # Ensure we have at least one test track
        self._ensure_test_content()

        logger.info(f"Music Player initialized. Streaming at http://localhost:{port}")

    def _ensure_test_content(self):
        """Generate test audio files if they don't exist."""
        test_file = os.path.join(self.content_dir, "test_tone.wav")
        if not os.path.exists(test_file):
            generate_tone(test_file, duration_sec=5, freq_hz=440)
            generate_tone(
                os.path.join(self.content_dir, "intro.wav"), duration_sec=3, freq_hz=554
            )  # C#5
            logger.info("Generated test audio content")

    def get_stream_url(self, filename: str) -> str:
        """Get the streaming URL for a file."""
        return f"http://localhost:{self.server.port}/{filename}"

    def __del__(self):
        """Cleanup on deletion."""
        if hasattr(self, "server"):
            self.server.stop()

    def load_playlist(self, tracks: list[dict[str, Any]]) -> None:
        """
        Load a playlist.

        Args:
            tracks: List of track dictionaries
        """
        self._playlist = tracks.copy()
        self._current_index = -1

        # Update server payload for dynamic API
        # Enrich tracks with full URLs
        api_playlist = []
        for track in self._playlist:
            t = track.copy()
            t["url"] = self.get_stream_url(track["filename"])
            api_playlist.append(t)

        self.server.set_playlist(api_playlist)

        logger.info(f"Loaded playlist with {len(tracks)} tracks")

    def add_track(self, track: dict[str, Any]) -> None:
        """Add a single track to the playlist."""
        self._playlist.append(track)

        # Update Server
        t = track.copy()
        t["url"] = self.get_stream_url(track["filename"])

        # Retrieve current payload and append to avoid full reload issues
        current = self.server.httpd.RequestHandlerClass.PAYLOAD if self.server.httpd else []
        current.append(t)
        self.server.set_playlist(current)

        logger.info(f"Added track to playlist: {track['title']}")

    def set_dj_message(self, message: str):
        """Update DJ message on server."""
        self.server.set_dj_message(message)

    def set_ledger_stats(self, stats: dict):
        """Update ledger stats on server."""
        self.server.set_ledger_stats(stats)

    def play(self, track_index: Optional[int] = None) -> bool:
        """
        Start playback.

        Args:
            track_index: Optional track index to play (default: next track)

        Returns:
            True if playback started, False otherwise
        """
        if not self._playlist:
            logger.warning("Cannot play: playlist is empty")
            return False

        if track_index is not None:
            if not 0 <= track_index < len(self._playlist):
                logger.error(f"Invalid track index: {track_index}")
                return False
            self._current_index = track_index
        else:
            # Play next track
            self._current_index = (self._current_index + 1) % len(self._playlist)

        self._current_track = self._playlist[self._current_index]
        self._state = PlayerState.PLAYING
        self._position_seconds = 0

        # Record playback start
        self._record_playback_event("play_start")

        logger.info(
            f"Playing: {self._current_track.get('title', 'Unknown')} by {self._current_track.get('artist', 'Unknown')}"
        )
        return True

    def pause(self) -> bool:
        """
        Pause playback.

        Returns:
            True if paused, False if not playing
        """
        if self._state != PlayerState.PLAYING:
            logger.warning("Cannot pause: not currently playing")
            return False

        self._state = PlayerState.PAUSED
        self._record_playback_event("pause")
        logger.info("Playback paused")
        return True

    def resume(self) -> bool:
        """
        Resume playback.

        Returns:
            True if resumed, False if not paused
        """
        if self._state != PlayerState.PAUSED:
            logger.warning("Cannot resume: not currently paused")
            return False

        self._state = PlayerState.PLAYING
        self._record_playback_event("resume")
        logger.info("Playback resumed")
        return True

    def stop(self) -> None:
        """Stop playback."""
        if self._current_track:
            self._record_playback_event("stop")

        self._state = PlayerState.STOPPED
        self._current_track = None
        self._position_seconds = 0
        logger.info("Playback stopped")

    def next_track(self) -> bool:
        """
        Skip to next track.

        Returns:
            True if skipped, False if at end of playlist
        """
        if not self._playlist:
            return False

        if self._current_track:
            self._record_playback_event("skip")

        next_index = (self._current_index + 1) % len(self._playlist)
        return self.play(next_index)

    def previous_track(self) -> bool:
        """
        Go to previous track.

        Returns:
            True if successful, False otherwise
        """
        if not self._playlist:
            return False

        prev_index = (self._current_index - 1) % len(self._playlist)
        return self.play(prev_index)

    def seek(self, position_seconds: int) -> bool:
        """
        Seek to position in current track.

        Args:
            position_seconds: Position in seconds

        Returns:
            True if successful, False otherwise
        """
        if not self._current_track:
            logger.warning("Cannot seek: no track playing")
            return False

        duration = self._current_track.get("duration", 0)
        if position_seconds < 0 or position_seconds > duration:
            logger.error(f"Invalid seek position: {position_seconds}")
            return False

        self._position_seconds = position_seconds
        self._record_playback_event("seek", {"position": position_seconds})
        logger.debug(f"Seeked to {position_seconds}s")
        return True

    def set_volume(self, volume: float) -> bool:
        """
        Set playback volume.

        Args:
            volume: Volume level (0.0-1.0)

        Returns:
            True if successful, False otherwise
        """
        if not 0.0 <= volume <= 1.0:
            logger.error(f"Invalid volume: {volume}")
            return False

        self._volume = volume
        logger.debug(f"Volume set to {volume:.1%}")
        return True

    def get_state(self) -> PlayerState:
        """Get current player state."""
        return self._state

    def get_current_track(self) -> Optional[dict[str, Any]]:
        """Get currently playing track."""
        return self._current_track

    def get_position(self) -> int:
        """Get current playback position in seconds."""
        return self._position_seconds

    def get_volume(self) -> float:
        """Get current volume level."""
        return self._volume

    def get_playlist(self) -> list[dict[str, Any]]:
        """Get current playlist."""
        return self._playlist.copy()

    def get_playlist_info(self) -> dict[str, Any]:
        """
        Get playlist information.

        Returns:
            Dictionary of playlist info
        """
        return {
            "track_count": len(self._playlist),
            "current_index": self._current_index,
            "current_track": self._current_track,
            "total_duration": sum(t.get("duration", 0) for t in self._playlist),
        }

    def get_playback_history(self, limit: int = 10) -> list[dict[str, Any]]:
        """
        Get playback history.

        Args:
            limit: Maximum number of events to return

        Returns:
            List of playback events
        """
        return self._playback_history[-limit:]

    def _record_playback_event(
        self, event_type: str, metadata: Optional[dict[str, Any]] = None
    ) -> None:
        """
        Record a playback event.

        Args:
            event_type: Type of event
            metadata: Optional event metadata
        """
        event = {
            "timestamp": datetime.now().isoformat(),
            "event_type": event_type,
            "track": self._current_track,
            "position": self._position_seconds,
            "metadata": metadata or {},
        }

        self._playback_history.append(event)

        # Keep only last 100 events
        if len(self._playback_history) > 100:
            self._playback_history = self._playback_history[-100:]

    def is_playing(self) -> bool:
        """Check if currently playing."""
        return self._state == PlayerState.PLAYING

    def is_paused(self) -> bool:
        """Check if currently paused."""
        return self._state == PlayerState.PAUSED

    def is_stopped(self) -> bool:
        """Check if stopped."""
        return self._state == PlayerState.STOPPED

__del__()

Cleanup on deletion.

Source code in qfzz/streaming/player.py
76
77
78
79
def __del__(self):
    """Cleanup on deletion."""
    if hasattr(self, "server"):
        self.server.stop()

__init__(content_dir='./audio_content', port=8000)

Initialize music player.

Parameters:

Name Type Description Default
content_dir str

Directory to serve audio from

'./audio_content'
port int

Streaming port

8000
Source code in qfzz/streaming/player.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def __init__(self, content_dir: str = "./audio_content", port: int = 8000):
    """
    Initialize music player.

    Args:
        content_dir: Directory to serve audio from
        port: Streaming port
    """
    self._state = PlayerState.STOPPED
    self._current_track: Optional[dict[str, Any]] = None
    self._playlist: list[dict[str, Any]] = []
    self._current_index = -1
    self._volume = 0.8
    self._position_seconds = 0
    self._playback_history: list[dict[str, Any]] = []

    # Setup content directory
    self.content_dir = os.path.abspath(content_dir)
    os.makedirs(self.content_dir, exist_ok=True)

    # Initialize streaming server
    self.server = StreamingServer(self.content_dir, port)
    self.server.start()

    # Ensure we have at least one test track
    self._ensure_test_content()

    logger.info(f"Music Player initialized. Streaming at http://localhost:{port}")

add_track(track)

Add a single track to the playlist.

Source code in qfzz/streaming/player.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def add_track(self, track: dict[str, Any]) -> None:
    """Add a single track to the playlist."""
    self._playlist.append(track)

    # Update Server
    t = track.copy()
    t["url"] = self.get_stream_url(track["filename"])

    # Retrieve current payload and append to avoid full reload issues
    current = self.server.httpd.RequestHandlerClass.PAYLOAD if self.server.httpd else []
    current.append(t)
    self.server.set_playlist(current)

    logger.info(f"Added track to playlist: {track['title']}")

get_current_track()

Get currently playing track.

Source code in qfzz/streaming/player.py
278
279
280
def get_current_track(self) -> Optional[dict[str, Any]]:
    """Get currently playing track."""
    return self._current_track

get_playback_history(limit=10)

Get playback history.

Parameters:

Name Type Description Default
limit int

Maximum number of events to return

10

Returns:

Type Description
list[dict[str, Any]]

List of playback events

Source code in qfzz/streaming/player.py
308
309
310
311
312
313
314
315
316
317
318
def get_playback_history(self, limit: int = 10) -> list[dict[str, Any]]:
    """
    Get playback history.

    Args:
        limit: Maximum number of events to return

    Returns:
        List of playback events
    """
    return self._playback_history[-limit:]

get_playlist()

Get current playlist.

Source code in qfzz/streaming/player.py
290
291
292
def get_playlist(self) -> list[dict[str, Any]]:
    """Get current playlist."""
    return self._playlist.copy()

get_playlist_info()

Get playlist information.

Returns:

Type Description
dict[str, Any]

Dictionary of playlist info

Source code in qfzz/streaming/player.py
294
295
296
297
298
299
300
301
302
303
304
305
306
def get_playlist_info(self) -> dict[str, Any]:
    """
    Get playlist information.

    Returns:
        Dictionary of playlist info
    """
    return {
        "track_count": len(self._playlist),
        "current_index": self._current_index,
        "current_track": self._current_track,
        "total_duration": sum(t.get("duration", 0) for t in self._playlist),
    }

get_position()

Get current playback position in seconds.

Source code in qfzz/streaming/player.py
282
283
284
def get_position(self) -> int:
    """Get current playback position in seconds."""
    return self._position_seconds

get_state()

Get current player state.

Source code in qfzz/streaming/player.py
274
275
276
def get_state(self) -> PlayerState:
    """Get current player state."""
    return self._state

get_stream_url(filename)

Get the streaming URL for a file.

Source code in qfzz/streaming/player.py
72
73
74
def get_stream_url(self, filename: str) -> str:
    """Get the streaming URL for a file."""
    return f"http://localhost:{self.server.port}/{filename}"

get_volume()

Get current volume level.

Source code in qfzz/streaming/player.py
286
287
288
def get_volume(self) -> float:
    """Get current volume level."""
    return self._volume

is_paused()

Check if currently paused.

Source code in qfzz/streaming/player.py
348
349
350
def is_paused(self) -> bool:
    """Check if currently paused."""
    return self._state == PlayerState.PAUSED

is_playing()

Check if currently playing.

Source code in qfzz/streaming/player.py
344
345
346
def is_playing(self) -> bool:
    """Check if currently playing."""
    return self._state == PlayerState.PLAYING

is_stopped()

Check if stopped.

Source code in qfzz/streaming/player.py
352
353
354
def is_stopped(self) -> bool:
    """Check if stopped."""
    return self._state == PlayerState.STOPPED

load_playlist(tracks)

Load a playlist.

Parameters:

Name Type Description Default
tracks list[dict[str, Any]]

List of track dictionaries

required
Source code in qfzz/streaming/player.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def load_playlist(self, tracks: list[dict[str, Any]]) -> None:
    """
    Load a playlist.

    Args:
        tracks: List of track dictionaries
    """
    self._playlist = tracks.copy()
    self._current_index = -1

    # Update server payload for dynamic API
    # Enrich tracks with full URLs
    api_playlist = []
    for track in self._playlist:
        t = track.copy()
        t["url"] = self.get_stream_url(track["filename"])
        api_playlist.append(t)

    self.server.set_playlist(api_playlist)

    logger.info(f"Loaded playlist with {len(tracks)} tracks")

next_track()

Skip to next track.

Returns:

Type Description
bool

True if skipped, False if at end of playlist

Source code in qfzz/streaming/player.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def next_track(self) -> bool:
    """
    Skip to next track.

    Returns:
        True if skipped, False if at end of playlist
    """
    if not self._playlist:
        return False

    if self._current_track:
        self._record_playback_event("skip")

    next_index = (self._current_index + 1) % len(self._playlist)
    return self.play(next_index)

pause()

Pause playback.

Returns:

Type Description
bool

True if paused, False if not playing

Source code in qfzz/streaming/player.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def pause(self) -> bool:
    """
    Pause playback.

    Returns:
        True if paused, False if not playing
    """
    if self._state != PlayerState.PLAYING:
        logger.warning("Cannot pause: not currently playing")
        return False

    self._state = PlayerState.PAUSED
    self._record_playback_event("pause")
    logger.info("Playback paused")
    return True

play(track_index=None)

Start playback.

Parameters:

Name Type Description Default
track_index Optional[int]

Optional track index to play (default: next track)

None

Returns:

Type Description
bool

True if playback started, False otherwise

Source code in qfzz/streaming/player.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def play(self, track_index: Optional[int] = None) -> bool:
    """
    Start playback.

    Args:
        track_index: Optional track index to play (default: next track)

    Returns:
        True if playback started, False otherwise
    """
    if not self._playlist:
        logger.warning("Cannot play: playlist is empty")
        return False

    if track_index is not None:
        if not 0 <= track_index < len(self._playlist):
            logger.error(f"Invalid track index: {track_index}")
            return False
        self._current_index = track_index
    else:
        # Play next track
        self._current_index = (self._current_index + 1) % len(self._playlist)

    self._current_track = self._playlist[self._current_index]
    self._state = PlayerState.PLAYING
    self._position_seconds = 0

    # Record playback start
    self._record_playback_event("play_start")

    logger.info(
        f"Playing: {self._current_track.get('title', 'Unknown')} by {self._current_track.get('artist', 'Unknown')}"
    )
    return True

previous_track()

Go to previous track.

Returns:

Type Description
bool

True if successful, False otherwise

Source code in qfzz/streaming/player.py
219
220
221
222
223
224
225
226
227
228
229
230
def previous_track(self) -> bool:
    """
    Go to previous track.

    Returns:
        True if successful, False otherwise
    """
    if not self._playlist:
        return False

    prev_index = (self._current_index - 1) % len(self._playlist)
    return self.play(prev_index)

resume()

Resume playback.

Returns:

Type Description
bool

True if resumed, False if not paused

Source code in qfzz/streaming/player.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def resume(self) -> bool:
    """
    Resume playback.

    Returns:
        True if resumed, False if not paused
    """
    if self._state != PlayerState.PAUSED:
        logger.warning("Cannot resume: not currently paused")
        return False

    self._state = PlayerState.PLAYING
    self._record_playback_event("resume")
    logger.info("Playback resumed")
    return True

seek(position_seconds)

Seek to position in current track.

Parameters:

Name Type Description Default
position_seconds int

Position in seconds

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in qfzz/streaming/player.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
def seek(self, position_seconds: int) -> bool:
    """
    Seek to position in current track.

    Args:
        position_seconds: Position in seconds

    Returns:
        True if successful, False otherwise
    """
    if not self._current_track:
        logger.warning("Cannot seek: no track playing")
        return False

    duration = self._current_track.get("duration", 0)
    if position_seconds < 0 or position_seconds > duration:
        logger.error(f"Invalid seek position: {position_seconds}")
        return False

    self._position_seconds = position_seconds
    self._record_playback_event("seek", {"position": position_seconds})
    logger.debug(f"Seeked to {position_seconds}s")
    return True

set_dj_message(message)

Update DJ message on server.

Source code in qfzz/streaming/player.py
118
119
120
def set_dj_message(self, message: str):
    """Update DJ message on server."""
    self.server.set_dj_message(message)

set_ledger_stats(stats)

Update ledger stats on server.

Source code in qfzz/streaming/player.py
122
123
124
def set_ledger_stats(self, stats: dict):
    """Update ledger stats on server."""
    self.server.set_ledger_stats(stats)

set_volume(volume)

Set playback volume.

Parameters:

Name Type Description Default
volume float

Volume level (0.0-1.0)

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in qfzz/streaming/player.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def set_volume(self, volume: float) -> bool:
    """
    Set playback volume.

    Args:
        volume: Volume level (0.0-1.0)

    Returns:
        True if successful, False otherwise
    """
    if not 0.0 <= volume <= 1.0:
        logger.error(f"Invalid volume: {volume}")
        return False

    self._volume = volume
    logger.debug(f"Volume set to {volume:.1%}")
    return True

stop()

Stop playback.

Source code in qfzz/streaming/player.py
193
194
195
196
197
198
199
200
201
def stop(self) -> None:
    """Stop playback."""
    if self._current_track:
        self._record_playback_event("stop")

    self._state = PlayerState.STOPPED
    self._current_track = None
    self._position_seconds = 0
    logger.info("Playback stopped")

Future Implementation

The current implementation is a placeholder. Future versions will include:

  • WebRTC Streaming: Real-time peer-to-peer audio streaming
  • HLS/DASH Support: Adaptive bitrate streaming protocols
  • DRM Integration: Digital rights management
  • P2P Distribution: Distributed content delivery
  • Buffer Management: Smart buffering strategies
  • Format Support: Multiple audio formats (MP3, OGG, FLAC, etc.)

Usage Example

from qfzz import MusicPlayer

# Create player
player = MusicPlayer()

# Play track
player.play_track("track_001")

# Pause
player.pause()

# Resume
player.resume()

# Stop
player.stop()

Planned API

Future versions will support:

# Advanced playback controls
player.seek(position_seconds)
player.set_volume(0.8)
player.set_playback_rate(1.0)

# Queue management
player.enqueue("track_002")
player.clear_queue()

# Events
@player.on('play')
def on_play(track_id):
    print(f"Playing: {track_id}")

@player.on('ended')
def on_ended(track_id):
    print(f"Ended: {track_id}")