class PlayManager(QWidget): NO_PLAYING_SONG = "没有正在播放的歌曲" def __init__(self, layout, parent=None): super().__init__(parent) self.player = QMediaPlayer(self) self.player.setAudioRole(QAudio.MusicRole) self.player.durationChanged.connect(self.duration_changed) self.player.positionChanged.connect(self.position_changed) self.player.mediaStatusChanged.connect(self.status_changed) self.player.bufferStatusChanged.connect(self.buffering_progress) self.player.stateChanged.connect(self.state_changed) self.player.error[QMediaPlayer.Error].connect( self.display_error_message) self.play_control = PlayControl(layout, parent) self.play_control.set_state(self.player.state()) self.play_control.play.connect(self.player.play) self.play_control.pause.connect(self.player.pause) self.play_control.stop.connect(self.player.stop) self.player.stateChanged.connect(self.play_control.set_state) self.song_label = QLabel() self.song_label.setText(self.NO_PLAYING_SONG) self.song_label.setWordWrap(True) song_grid = QGridLayout(parent) song_grid.addWidget(self.song_label, 0, 0) self.duration_label = QLabel() self.duration_label.setText("00:00 / 00:00") self.slider = QSlider(Qt.Horizontal, parent) self.slider.setRange(0, self.player.duration() / 1000) self.slider.sliderMoved.connect(self.seek) progress_layout = QHBoxLayout(parent) progress_layout.insertWidget(0, self.duration_label) progress_layout.insertWidget(0, self.slider) song_grid.addLayout(progress_layout, 1, 0) self.status_label = QLabel() song_grid.addWidget(self.status_label, 2, 0) layout.addLayout(song_grid) if not self.is_player_available(): QMessageBox.warning(self, "警告", "QMediaPlayer 对象无可用的服务") return self.song = None def is_player_available(self): return self.player.isAvailable() def add_to_play(self, spider, song): self.song = song spider.get_song_url(song) url = song.url if url is None or len(url) == 0: return req = QNetworkRequest(QUrl(url)) req.setHeader(QNetworkRequest.UserAgentHeader, HttpRequestConfig.CHROME_USER_AGENT) self.player.setMedia(QMediaContent(req)) def duration_changed(self, duration): self.duration = int(duration / 1000) self.slider.setMaximum(self.duration) t = "00:00 / %s" % (self.song.get_time()) self.duration_label.setText(t) def position_changed(self, progress): if not self.slider.isSliderDown(): self.slider.setValue(int(progress / 1000)) self.update_duration_info(int(progress / 1000)) @pyqtSlot(int) def seek(self, seconds): self.player.setPosition(seconds * 1000) def state_changed(self, state): if QMediaPlayer.PlayingState == state \ or QMediaPlayer.PausedState == state: self.song_label.setText("%s - %s" % (self.song.get_singers(), self.song.name)) else: self.song_label.setText(self.NO_PLAYING_SONG) def status_changed(self, status): if QMediaPlayer.UnknownMediaStatus == status \ or QMediaPlayer.NoMedia == status \ or QMediaPlayer.LoadedMedia == status \ or QMediaPlayer.BufferedMedia == status: self.set_status_info("") elif QMediaPlayer.LoadingMedia == status: self.set_status_info("加载中...") elif QMediaPlayer.BufferingMedia == status: self.set_status_info("缓存中...") elif QMediaPlayer.StalledMedia == status: self.set_status_info("等待中...") elif QMediaPlayer.EndOfMedia == status: QApplication.alert(self) elif QMediaPlayer.InvalidMedia == status: self.display_error_message() @pyqtSlot(int) def buffering_progress(self, progress): if self.player.mediaStatus() == QMediaPlayer.StalledMedia: self.set_status_info("等待中 %d%%" % (progress, )) else: self.set_status_info("缓存中 %d%%" % (progress, )) def set_status_info(self, info): self.status_label.setText(info) def display_error_message(self): self.set_status_info(self.player.errorString()) def update_duration_info(self, current_info): s = time.strftime("%M:%S", time.gmtime(current_info)) t = "%s / %s" % (s, self.song.get_time()) self.duration_label.setText(t)
class Control: """A class that handles the logic behind the program by manipulating the GUI classes and calling their methods in response to received signals.""" MAGIC = b"\x01\xff" def __init__(self, screens: list) -> None: self.player = QMediaPlayer() self.player.setAudioRole(QAudio.MusicRole) self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.mainWindow = MainWindow(self, screens) self.mainArea = self.mainWindow.centralWidget().upperBox.mainArea self.songList = self.mainWindow.centralWidget().upperBox.songList self.mediaControlArea = self.mainWindow.centralWidget( ).mediaControlArea self.mainWindow.show() self.library = None self.currentSong = None self.playing = False self.random = False self.repeat = 0 self.volume = 50 self.volumeChange(self.volume) self.mediaControlArea.volumeControl.setValue(self.volume) self.mainTimer = QTimer() self.mainTimer.setInterval(100) self.mainTimer.timeout.connect(self.updateSongProgress) self.mainTimer.start() self.libraryUpdateTimer = QTimer() self.libraryUpdateTimer.setInterval(15_000) self.libraryUpdateTimer.timeout.connect(self.updateLibrary) self.libraryUpdateTimer.start() self._connection = None self.connections() self.displayedType = None self.displayedName = None self.types = None self.songListWidth = None values = self.load() if values: self.mainWindow.setGeometry(*values) self.volumeChange(self.volume) self.mediaControlArea.volumeControl.setValue(self.volume) self.setUpTimer = QTimer() self.setUpTimer.setInterval(20) self.setUpTimer.setSingleShot(True) self.setUpTimer.timeout.connect(self.setAreas) self.setUpTimer.start() def connections(self): self.player.currentMediaChanged.connect(self.updateCurrentSong) self.player.durationChanged.connect(self.updateSongProgressRange) self.player.stateChanged.connect(self.playerStatusChanged) self.mediaControlArea.previousButton.click.connect( self.playlist.previous) self.mediaControlArea.repeatButton.click.connect( self.repeatButtonClick) self.mediaControlArea.stopButton.click.connect(self.stopButtonClick) self.mediaControlArea.playButton.click.connect(self.playButtonClick) self.mediaControlArea.randomButton.click.connect( self.randomButtonClick) self.mediaControlArea.nextButton.click.connect(self.playlist.next) self.mediaControlArea.muteButton.click.connect(self.mute) self.mediaControlArea.songProgress.sliderMoved.connect( self.songProgressMove) self.mediaControlArea.volumeControl.sliderMoved.connect( self.volumeChange) def setAreas(self) -> None: """Called after the GUI is created to provide user with a feedback that the program is running in case a larger amount of songs will be added when the Library class is initialized.""" # TODO add a tooltip that will notify the user larger amount of songs is being loaded # (freezes the program as the execution moves to the Library class.) self.library = library.Library() self.types = { "artist": self.library.getSongsForArtist, "album": self.library.getSongsForAlbum, "playlist": self.library.getSongsForPlaylist } self.mainArea.setAreas(self.library) self.setUpTimer.deleteLater() self.setUpTimer = None self.getSongs(self.displayedType, self.displayedName) if self.songListWidth is not None: songListGeometry = self.songList.geometry() self.songList.preferredWidth = songListGeometry.width( ) - self.songListWidth self.mainWindow.centralWidget().upperBox.line.resizeWidgets( songListGeometry.width() - self.songListWidth) def updateLibrary(self) -> None: self.library.update() self.mainArea.updateView(self.library) def updateCurrentSong(self) -> None: """Update all areas that may display information about the currently playing song - SongList, Now Playing tab, BottomBox""" media = self.player.currentMedia() self.currentSong = media.request().url().toLocalFile().replace( "/", "\\") if self.currentSong in self.library.library: self.songList.updateActiveSong(self.currentSong) self.mainArea.updateActiveSong(self.playlist.currentIndex()) songEntry = self.library.library[self.currentSong] self.mediaControlArea.updateSongInfo( f"{songEntry[ARTIST]} - {songEntry[NAME]}") def updateSongProgressRange(self) -> None: """Updates the range of the slider that represents the song position.""" self.mediaControlArea.updateSongProgressRange(self.player.duration()) def playerStatusChanged(self) -> None: """Used to properly update the player look after the current playlist has finished.""" index = self.playlist.currentIndex() if index == -1: self.stopButtonClick() def getSongs(self, isType: str, name: str) -> None: """Retrieves the songs for a given artist, album or playlist based on type and passes the resulting list to the SongList class.""" if isType is None: isType = self.displayedType if name is None: name = self.displayedName orderBy = self.songList.buttonOrderBy.text() reverse = True if self.songList.buttonOrderReverse.text() == chr( 0x25bc) else False listForType = self.types[isType](name, orderBy, reverse) playlist = None if isType == "playlist": playlist = name if len(listForType) == 0: artists = self.library.artists if len(artists): listForType = self.library.getSongsForArtist(artists[0]) self.songList.updateSongList(listForType, self.library.library, self.currentSong, playlist, isType) self.displayedType = isType self.displayedName = name def playSongList(self, song: str = None) -> None: """Called when user double-clicks on an artist/album/playlist widget or a song in right-hand side panel.""" self.playlist.clear() index = 0 loopIndex = 0 for songPath in self.songList.garbageProtector: if song == songPath: index = loopIndex self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(songPath))) loopIndex += 1 if self.playlist.isEmpty(): return self.player.play() if index > 0: self.playlist.setCurrentIndex(index) self.playing = True self.mediaControlArea.playButton.updatePictures( bottom.pausePixmap, bottom.pauseHoverPixmap, False) self.mainArea.setNowPlayingArea(self.library) self.mainArea.updateActiveSong(self.playlist.currentIndex()) def playSongWidget(self, songPath: str, afterCurrent: bool = False) -> None: if afterCurrent: index = self.playlist.currentIndex() + 1 self.playlist.insertMedia( index, QMediaContent(QUrl.fromLocalFile(songPath))) else: self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(songPath))) self.mainArea.setNowPlayingArea(self.library) self.mainArea.updateActiveSong(self.playlist.currentIndex()) self.playing = True def removeFromNowPlaying(self, widget) -> None: if self.playlist.mediaCount() > 1: for row in range(self.mainArea.nowPlayingLayout.rowCount()): for column in range( self.mainArea.nowPlayingLayout.columnCount()): if self.mainArea.nowPlayingLayout.itemAtPosition( row, column).widget() is widget: self.playlist.removeMedia(row - 1) break else: continue break else: self.stopButtonClick() self.playlist.clear() self.mainArea.setNowPlayingArea(self.library) if self.playing: self.mainArea.updateActiveSong(self.playlist.currentIndex()) def playMediaWidget(self, isType: str, target: str, startOver: bool, afterCurrent: bool) -> None: """Called from MediaWidget - plays all songs for MediaWidget's type and name.""" if startOver: self.playlist.clear() if afterCurrent: index = self.playlist.currentIndex() + 1 for songPath in self.types[isType](target): self.playlist.insertMedia( index, QMediaContent(QUrl.fromLocalFile(songPath))) index += 1 else: for songPath in self.types[isType](target): self.playlist.addMedia( QMediaContent(QUrl.fromLocalFile(songPath))) if startOver: self.player.play() self.playing = True self.mediaControlArea.playButton.updatePictures( bottom.pausePixmap, bottom.pauseHoverPixmap, False) self.mainArea.setNowPlayingArea(self.library) self.mainArea.updateActiveSong(self.playlist.currentIndex()) def playFromNowPlaying(self, song: str) -> None: """Called when user double-clicks on a song in the Now Playing tab.""" for n in range(self.playlist.mediaCount()): media = self.playlist.media(n) if song == media.request().url().toLocalFile().replace("/", "\\"): self.playlist.setCurrentIndex(n) if not self.playing: self.player.play() self.playing = True return def createPlaylist(self, playlistName: str) -> None: self.library.createPlaylist(playlistName) self.mainArea.setMainAreaPlaylists(self.library) def addToExistingPlaylist(self, playlist: str, songOrWidget: str, isType: str) -> None: if isType in self.types: for song in self.types[isType](songOrWidget): self.library.addToPlaylist(playlist, song) else: self.library.addToPlaylist(playlist, songOrWidget) self.library.update() def removeFromPlaylist(self, playlist: str, song: str) -> None: self.library.deleteFromPlaylist(playlist, song) self.mainArea.setMainAreaPlaylists(self.library) self.library.update() self.getSongs("playlist", playlist) def renamePlaylist(self, playlistName: str, newPlaylistName: str) -> None: self.library.renamePlaylist(playlistName, newPlaylistName) self.mainArea.setMainAreaPlaylists(self.library) self.library.update() def deletePlaylist(self, playlistName: str) -> None: self.library.deletePlaylist(playlistName) self.mainArea.setMainAreaPlaylists(self.library) self.library.update() def addWatchedFolder(self, folder: str) -> None: """Adds a folder to the Library class. all mp3 files within the folder and its sub-folders will be added to the library and accessible to the player.""" self.library.addFolder(folder.replace("/", "\\")) self.mainArea.updateView(self.library) def removeWatchedFolder(self, folder: str) -> None: """Removes folder from the library, updates view and stops playback if the current song was in the now-removed folder.""" self.library.deleteFolder(folder) self.mainArea.updateView(self.library) if self.currentSong not in self.library.library: self.songList.updateSongList([], [], "", "") self.player.stop() self.playlist.clear() self.mediaControlArea.updateSongInfo("") self.songList.nowPlayingSong = None self.mainArea.nowPlayingSong = None self.playing = False self.mediaControlArea.updatePlayButton(self.playing, False) def playButtonClick(self, passMove: bool = True) -> None: if not self.playing: if self.playlist.isEmpty(): self.playSongList() return self.playing = True self.player.play() self.mainArea.updateActiveSong(self.playlist.currentIndex()) self.songList.updateActiveSong(self.currentSong) else: self.playing = False self.player.pause() self.mediaControlArea.updatePlayButton(self.playing, passMove) def repeatButtonClick(self) -> None: if self.repeat == 0: self.repeat = 1 self.playlist.setPlaybackMode(QMediaPlaylist.Loop) elif self.repeat == 1: self.repeat = 2 self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop) elif self.repeat == 2: self.repeat = 0 self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) self.mediaControlArea.updateRepeatButton(self.repeat) def randomButtonClick(self) -> None: if not self.random: self.random = True self.playlist.setPlaybackMode(QMediaPlaylist.Random) else: self.random = False self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) self.mediaControlArea.updateRandomButton(self.random) def stopButtonClick(self) -> None: self.playing = False self.player.stop() if self.songList.nowPlayingSong is not None: self.songList.nowPlayingSong.clear() if self.mainArea.nowPlayingSong is not None: self.mainArea.nowPlayingSong.clear() self.mediaControlArea.updatePlayButton(self.playing, False) def mute(self) -> None: if not self.player.isMuted(): self.player.setMuted(True) self.mediaControlArea.showMute() else: self.player.setMuted(False) self.volumeChange(self.volume) def volumeChange(self, volume: int) -> None: logVolume = QAudio.convertVolume(volume / 100, QAudio.LogarithmicVolumeScale, QAudio.LinearVolumeScale) * 100 self.player.setVolume(logVolume) self.volume = volume self.mediaControlArea.updateVolumeBar(volume) def songProgressMove(self, position: int) -> None: self.player.setPosition(position) def updateSongProgress(self) -> None: position = self.player.position() if 0 <= position < 2_000_000_000: if self.player.state() > 0: self.mediaControlArea.updateSongProgress(position) if self.playing: self.songList.activeSongPixmap() self.mainArea.activeSongPixmap() else: self.mediaControlArea.updateSongProgress(0) def disconnect(self): self.player.currentMediaChanged.disconnect() self.player.durationChanged.disconnect() self.player.stateChanged.disconnect() self.mediaControlArea.previousButton.click.disconnect() self.mediaControlArea.repeatButton.click.disconnect() self.mediaControlArea.stopButton.click.disconnect() self.mediaControlArea.playButton.click.disconnect() self.mediaControlArea.randomButton.click.disconnect() self.mediaControlArea.nextButton.click.disconnect() self.mediaControlArea.muteButton.click.disconnect() self.mediaControlArea.songProgress.sliderMoved.disconnect() self.mediaControlArea.volumeControl.sliderMoved.disconnect() def close(self) -> None: self.disconnect() self.player.stop() self.mainTimer.stop() self.save() def save(self) -> None: """Called on exit, saves current view, geometry and volume.""" with gzip.open(r"musicplayer\mpdata", "wb") as fh: fh.write(self.MAGIC) toBeWritten = struct.pack(f"<h{len(self.displayedType.encode())}s", len(self.displayedType.encode()), self.displayedType.encode()) fh.write(toBeWritten) fh.write(self.MAGIC) toBeWritten = struct.pack(f"<h{len(self.displayedName.encode())}s", len(self.displayedName.encode()), self.displayedName.encode()) fh.write(toBeWritten) fh.write(self.MAGIC) geo = self.mainWindow.geometry() toBeWritten = struct.pack("<4h", geo.x(), geo.y(), geo.width(), geo.height()) fh.write(toBeWritten) toBeWritten = struct.pack("<h", self.volume) fh.write(toBeWritten) toBeWritten = struct.pack("<h", self.songList.width()) fh.write(toBeWritten) def load(self) -> [bool, tuple]: """Called on startup, loads view, geometry and volume saved on previous run.""" try: with gzip.open(r"musicplayer\mpdata", "rb") as fh: if not fh.read(2) == self.MAGIC: return False length = fh.read(2) length = struct.unpack("<h", length)[0] displayedType = fh.read(length) displayedType = struct.unpack(f"<{length}s", displayedType)[0].decode("utf8") if displayedType in ["artist", "album", "playlist"]: self.displayedType = displayedType if not fh.read(2) == self.MAGIC: return False length = fh.read(2) length = struct.unpack("<h", length)[0] displayedName = fh.read(length) displayedName = struct.unpack(f"<{length}s", displayedName)[0].decode("utf8") if not fh.read(2) == self.MAGIC: return False self.displayedName = displayedName variables = [] for n in range(6): var = fh.read(2) var = struct.unpack("<h", var)[0] variables.append(var) x, y, width, height, volume, songListWidth = variables self.volume = volume self.songListWidth = songListWidth return x, y, width, height except Exception: return False
class MyQtApp(multimediaUI.Ui_MainWindow, QtWidgets.QMainWindow): def __init__(self): super().__init__() self.setupUi(self) self.setWindowTitle("Multimedia Project") self.play_btn.clicked.connect(self.play) self.pause_btn.clicked.connect(self.pause) self.stop_btn.clicked.connect(self.stop) self.play_btn.setEnabled(False) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) #read metadata file_meta = open('final.json', "r") self.metadata = json.load(file_meta) self.num_img = len(self.metadata) file_meta.close() # synopsis = readrgb.readrgbtoQImage("version2_synopsis.rgb", 352*self.num_img, 288) synopsis = readrgb.readrgbtoQImage("final.rgb", 352 * self.num_img, 288) #print(self.synopsis.size()) self.total_length = 120 * self.num_img # synopsis from 0 to total length pixmap_syn = QPixmap.fromImage(synopsis).scaled( 120 * self.num_img, 1440, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.synopsis.setPixmap(pixmap_syn) self.synopsis.mousePressEvent = self.getPos self.sound = QVideoWidget() self.sound.setGeometry(QtCore.QRect(859, 10, 111, 21)) self.sound.setObjectName("sound") self.soundPlayer = QMediaPlayer(None, QMediaPlayer.LowLatency) self.soundPlayer.setAudioRole(2) self.v_thread = Img_Thread(self.updateframe) self.v_thread.signal.connect(self.stop) self.v_thread.n = 0 self.v_thread.start() self.sound_delay = 0.2 def play(self): #print("play") #if self.soundPlayer.state() != QMediaPlayer.PlayingState: #print(self.soundPlayer.position()/1000) #time.sleep(0.05) #print(self.v_thread.isFinished()) self.thistimestart_frame = self.current_frame - 1 self.soundPlayer.play() time.sleep(self.sound_delay) # wait for the sound track to play # while self.soundPlayer.position() <= 1: # pass self.tic = time.perf_counter() self.image_thread() #self.soundPlayer.play() self.play_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) def updateframe(self): #print(self.current_frame) fileName = "image-" + str(self.current_frame).zfill(4) + ".rgb" video = readrgb.readrgbtoQImage(self.folderName + fileName) pixmap_vdo = QPixmap.fromImage(video) ontime = (self.current_frame - self.thistimestart_frame) / 30 delay = time.perf_counter() - self.tic if delay < ontime: #if (ontime-delay) > 0.033: # print("long wait", ontime-delay) time.sleep(ontime - delay) else: print("too late", delay - ontime) self.video.setPixmap(pixmap_vdo) #print(self.current_frame) self.current_frame += 1 def image_thread(self): self.v_thread.n = self.end_frame - self.current_frame + 1 self.v_thread.kill = 0 self.v_thread.start() def pause(self): #print("pause") #if self.soundPlayer.state() == QMediaPlayer.PlayingState: self.soundPlayer.pause() self.v_thread.kill = 1 time.sleep(0.04) #print(self.v_thread.isFinished()) print(self.current_frame) self.current_frame = self.soundPlayer.position() * 30 // 1000 + 1 print(self.current_frame) self.play_btn.setEnabled(True) self.pause_btn.setEnabled(False) def stop(self): #print("stop") if self.soundPlayer.state() != QMediaPlayer.StoppedState: self.soundPlayer.pause() #print(self.soundPlayer.position()/1000) print(self.soundPlayer.position() / 1000 - self.end_frame / 30) self.v_thread.kill = 1 self.current_frame = self.start_frame self.soundPlayer.setPosition(self.start_time) self.play_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) def show_img(self): if self.soundPlayer.state() != QMediaPlayer.StoppedState: self.stop() self.play_btn.setEnabled(False) video = readrgb.readrgbtoQImage("." + self.fileName) pixmap_vdo = QPixmap.fromImage(video) self.video.setPixmap(pixmap_vdo) self.Displaying.setText("Displaying Image: " + self.fileName) def play_video(self): self.current_frame = self.start_frame self.soundPlayer.setMedia( QMediaContent(QUrl.fromLocalFile(os.path.abspath( self.audio_file)))) # mac # self.soundPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(self.audio_file))) self.soundPlayer.setPosition(self.start_time) fileName = "image-" + str(self.start_frame).zfill(4) + ".rgb" video = readrgb.readrgbtoQImage(self.folderName + fileName) pixmap_vdo = QPixmap.fromImage(video) self.current_frame += 1 self.video.setPixmap(pixmap_vdo) self.Displaying.setText("Displaying Video: " + self.folderName) self.play() def getfiles(self, idx): #print(idx) if idx >= self.num_img: idx = self.num_img - 1 tp = self.metadata[idx]["tp"] # tp = 1: video ; tp = 0: image if tp == 1: # for video self.folderName = "." + self.metadata[idx][ "folder"] #"../../576RGBVideo1/" # self.folderName = "/Users/luckyjustin/Documents/JustinProject/576Project/CSCI576ProjectMedia/576RGBVideo1/" self.start_frame = self.metadata[idx]["start"] #1 self.end_frame = self.metadata[idx]["end"] #1000 self.start_time = (self.start_frame - 1) * 1000 / 30 self.audio_file = "." + self.metadata[idx]["audio"] #"video_1.wav" #self.audio_file = "/Users/luckyjustin/Documents/JustinProject/576Project/CSCI576ProjectMedia/video_1.wav" #print(self.folderName) #print(self.audio_file) #print("start frame = " + str(self.start_frame/30)) #print("end frame = " + str(self.end_frame/30)) else: # for image self.fileName = self.metadata[idx]["path"] #"image-0003.rgb" return tp # tp = 1: video ; tp = 0: image def getPos(self, event): x = event.pos().x() #print(x) tp = self.getfiles(x // 120) # x//(width of an image = 120) if self.soundPlayer.state() == QMediaPlayer.PlayingState: self.v_thread.kill = 1 time.sleep(0.04) # for racing if tp: self.play_video() else: self.show_img()