class VideoWidget(QFrame): mutedChanged = pyqtSignal(list) volumeChanged = pyqtSignal(list) addMedia = pyqtSignal(list) # 发送新增的直播 deleteMedia = pyqtSignal(int) # 删除选中的直播 exchangeMedia = pyqtSignal(list) # 交换播放窗口 setDanmu = pyqtSignal() # 发射弹幕设置信号 setTranslator = pyqtSignal(list) # 发送同传关闭信号 changeQuality = pyqtSignal(list) # 修改画质 changeAudioChannel = pyqtSignal(list) # 修改音效 popWindow = pyqtSignal(list) # 弹出悬浮窗 hideBarKey = pyqtSignal() # 隐藏控制条快捷键 fullScreenKey = pyqtSignal() # 全屏快捷键 muteExceptKey = pyqtSignal() # 除了这个播放器 其他全部静音快捷键 def __init__(self, id, volume, cacheFolder, top=False, title='', resize=[], textSetting=[True, 20, 2, 6, 0, '【 [ {'], maxCacheSize=2048000, startWithDanmu=True): super(VideoWidget, self).__init__() self.setAcceptDrops(True) self.installEventFilter(self) self.id = id self.title = '未定义的直播间' self.uname = '未定义' self.hoverToken = False self.roomID = '0' # 初始化直播间房号 self.liveStatus = 0 # 初始化直播状态为0 self.pauseToken = False self.quality = 250 self.audioChannel = 0 # 0 原始音效 5 杜比音效 self.volume = volume self.volumeAmplify = 1.0 # 音量加倍 self.hardwareDecode = True self.leftButtonPress = False self.rightButtonPress = False self.fullScreen = False self.userPause = False # 用户暂停 self.cacheName = '' self.maxCacheSize = maxCacheSize self.startWithDanmu = startWithDanmu self.setFrameShape(QFrame.Box) self.setObjectName('video') self.top = top if top: # 悬浮窗取消关闭按钮 vlc版点关闭后有bug 让用户右键退出 self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint) else: self.setStyleSheet( '#video{border-width:1px;border-style:solid;border-color:gray}' ) self.textSetting = textSetting self.horiPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][self.textSetting[2]] self.vertPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][self.textSetting[3]] self.filters = textSetting[5].split(' ') self.opacity = 100 if top: self.setWindowFlag(Qt.WindowStaysOnTopHint) if title: if top: self.setWindowTitle('%s %s' % (title, id + 1 - 9)) else: self.setWindowTitle('%s %s' % (title, id + 1)) if resize: self.resize(resize[0], resize[1]) layout = QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.textBrowser = TextBrowser( self) # 必须赶在resizeEvent和moveEvent之前初始化textbrowser self.setDanmuOpacity(self.textSetting[1]) # 设置弹幕透明度 self.textBrowser.optionWidget.opacitySlider.setValue( self.textSetting[1]) # 设置选项页透明条 self.textBrowser.optionWidget.opacitySlider.value.connect( self.setDanmuOpacity) self.setHorizontalPercent(self.textSetting[2]) # 设置横向占比 self.textBrowser.optionWidget.horizontalCombobox.setCurrentIndex( self.textSetting[2]) # 设置选项页占比框 self.textBrowser.optionWidget.horizontalCombobox.currentIndexChanged.connect( self.setHorizontalPercent) self.setVerticalPercent(self.textSetting[3]) # 设置横向占比 self.textBrowser.optionWidget.verticalCombobox.setCurrentIndex( self.textSetting[3]) # 设置选项页占比框 self.textBrowser.optionWidget.verticalCombobox.currentIndexChanged.connect( self.setVerticalPercent) self.setTranslateBrowser(self.textSetting[4]) self.textBrowser.optionWidget.translateCombobox.setCurrentIndex( self.textSetting[4]) # 设置同传窗口 self.textBrowser.optionWidget.translateCombobox.currentIndexChanged.connect( self.setTranslateBrowser) self.setTranslateFilter(self.textSetting[5]) # 同传过滤字符 self.textBrowser.optionWidget.translateFitler.setText( self.textSetting[5]) self.textBrowser.optionWidget.translateFitler.textChanged.connect( self.setTranslateFilter) self.textBrowser.closeSignal.connect(self.closeDanmu) self.textBrowser.moveSignal.connect(self.moveTextBrowser) if not self.startWithDanmu: # 如果启动隐藏被设置,隐藏弹幕机 self.textSetting[0] = False self.textBrowser.hide() self.textPosDelta = QPoint(0, 0) # 弹幕框和窗口之间的坐标差 self.videoFrame = VideoFrame() # 新版本vlc内核播放器 self.videoFrame.rightClicked.connect(self.rightMouseClicked) self.videoFrame.leftClicked.connect(self.leftMouseClicked) self.videoFrame.doubleClicked.connect(self.doubleClick) layout.addWidget(self.videoFrame, 0, 0, 12, 12) self.instance = vlc.Instance() self.player = self.instance.media_player_new() # 视频播放 self.player.video_set_mouse_input(False) self.player.video_set_key_input(False) if platform.system() == 'Windows': self.player.set_hwnd(self.videoFrame.winId()) elif platform.system() == 'Darwin': # for MacOS self.player.set_nsobject(int(self.videoFrame.winId())) else: self.player.set_xwindow(self.videoFrame.winId()) self.topLabel = QLabel() self.topLabel.setFixedHeight(30) # self.topLabel.setAlignment(Qt.AlignCenter) self.topLabel.setObjectName('frame') self.topLabel.setStyleSheet("background-color:#293038") # self.topLabel.setFixedHeight(32) self.topLabel.setFont(QFont('微软雅黑', 15, QFont.Bold)) layout.addWidget(self.topLabel, 0, 0, 1, 12) self.topLabel.hide() self.frame = QWidget() self.frame.setObjectName('frame') self.frame.setStyleSheet("background-color:#293038") self.frame.setFixedHeight(50) frameLayout = QHBoxLayout(self.frame) frameLayout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.frame, 11, 0, 1, 12) self.frame.hide() self.titleLabel = QLabel() self.titleLabel.setMaximumWidth(150) self.titleLabel.setStyleSheet('background-color:#00000000') self.setTitle() frameLayout.addWidget(self.titleLabel) self.play = PushButton(self.style().standardIcon(QStyle.SP_MediaPause)) self.play.clicked.connect(self.mediaPlay) frameLayout.addWidget(self.play) self.reload = PushButton(self.style().standardIcon( QStyle.SP_BrowserReload)) self.reload.clicked.connect(self.mediaReload) frameLayout.addWidget(self.reload) self.volumeButton = PushButton(self.style().standardIcon( QStyle.SP_MediaVolume)) self.volumeButton.clicked.connect(self.mediaMute) frameLayout.addWidget(self.volumeButton) self.slider = Slider() self.slider.setStyleSheet('background-color:#00000000') self.slider.value.connect(self.setVolume) frameLayout.addWidget(self.slider) self.danmuButton = PushButton(text='弹') self.danmuButton.clicked.connect(self.showDanmu) frameLayout.addWidget(self.danmuButton) self.stop = PushButton(self.style().standardIcon( QStyle.SP_DialogCancelButton)) self.stop.clicked.connect(self.mediaStop) frameLayout.addWidget(self.stop) self.getMediaURL = GetMediaURL(self.id, cacheFolder, maxCacheSize) self.getMediaURL.cacheName.connect(self.setMedia) self.getMediaURL.downloadError.connect(self.mediaReload) self.danmu = remoteThread(self.roomID) self.exportCache = ExportCache() self.exportCache.finish.connect(self.exportFinish) self.exportTip = ExportTip() self.moveTimer = QTimer() self.moveTimer.timeout.connect(self.initTextPos) self.moveTimer.start(50) self.checkPlaying = QTimer() # 检查播放卡住的定时器 self.checkPlaying.timeout.connect(self.checkPlayStatus) logging.info("VLC 播放器构造完毕, 缓存大小: %dkb , 置顶?: %s, 启用弹幕?: %s" % (self.maxCacheSize, self.top, self.startWithDanmu)) def checkPlayStatus(self): # 播放卡住了 if not self.player.is_playing() and not self.isHidden( ) and self.liveStatus != 0 and not self.userPause: self.mediaReload() # 刷新一下 def initTextPos(self): # 初始化弹幕机位置 videoPos = self.mapToGlobal(self.videoFrame.pos()) if self.textBrowser.pos() != videoPos: self.textBrowser.move(videoPos) else: self.moveTimer.stop() def setDanmuOpacity(self, value): if value < 7: value = 7 # 最小透明度 self.textSetting[1] = value # 记录设置 value = int(value / 101 * 256) color = str(hex(value))[2:] + '000000' self.textBrowser.textBrowser.setStyleSheet('background-color:#%s' % color) self.textBrowser.transBrowser.setStyleSheet('background-color:#%s' % color) self.setDanmu.emit() def setHorizontalPercent(self, index): # 设置弹幕框水平宽度 self.textSetting[2] = index self.horiPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][index] # 记录横向占比 width = self.width() * self.horiPercent self.textBrowser.resize(width, self.textBrowser.height()) if width > 240: self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', 17, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', 17, QFont.Bold)) elif 100 < width <= 240: self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) else: self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', 10, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', 10, QFont.Bold)) self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) self.setDanmu.emit() def setVerticalPercent(self, index): # 设置弹幕框垂直高度 self.textSetting[3] = index self.vertPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][index] # 记录纵向占比 self.textBrowser.resize(self.textBrowser.width(), self.height() * self.vertPercent) self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) self.setDanmu.emit() def setTranslateBrowser(self, index): self.textSetting[4] = index if index == 0: # 显示弹幕和同传 self.textBrowser.textBrowser.show() self.textBrowser.transBrowser.show() elif index == 1: # 只显示弹幕 self.textBrowser.transBrowser.hide() self.textBrowser.textBrowser.show() elif index == 2: # 只显示同传 self.textBrowser.textBrowser.hide() self.textBrowser.transBrowser.show() self.textBrowser.resize(self.width() * self.horiPercent, self.height() * self.vertPercent) self.setDanmu.emit() def setTranslateFilter(self, filterWords): self.textSetting[5] = filterWords self.filters = filterWords.split(' ') self.setDanmu.emit() def resizeEvent(self, QEvent): width = self.width() * self.horiPercent self.textBrowser.resize(width, self.height() * self.vertPercent) if width > 300: self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', 16, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', 16, QFont.Bold)) elif 240 < width <= 300: self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', width // 20 + 1, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', width // 20 + 1, QFont.Bold)) else: self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', 12, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', 12, QFont.Bold)) self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) self.moveTextBrowser() def moveEvent( self, QMoveEvent): # 理论上给悬浮窗同步弹幕机用的moveEvent 但不生效 但是又不能删掉 不然交换窗口弹幕机有bug videoPos = self.mapToGlobal( self.videoFrame.pos()) # videoFrame的坐标要转成globalPos self.textBrowser.move(videoPos + self.textPosDelta) self.textPosDelta = self.textBrowser.pos() - videoPos def moveTextBrowser(self, point=None): videoPos = self.mapToGlobal( self.videoFrame.pos()) # videoFrame的坐标要转成globalPos videoX, videoY = videoPos.x(), videoPos.y() videoW, videoH = self.videoFrame.width(), self.videoFrame.height() if point: danmuX, danmuY = point.x(), point.y() else: danmuX, danmuY = self.textBrowser.x(), self.textBrowser.y( ) # textBrowser坐标本身就是globalPos danmuW, danmuH = self.textBrowser.width(), self.textBrowser.height() smaller = False # 弹幕机尺寸大于播放窗 if danmuW > videoW or danmuH > videoH: danmuX, danmuY = videoX, videoY smaller = True if not smaller: if danmuX < videoX: danmuX = videoX elif danmuX > videoX + videoW - danmuW: danmuX = videoX + videoW - danmuW if danmuY < videoY: danmuY = videoY elif danmuY > videoY + videoH - danmuH: danmuY = videoY + videoH - danmuH self.textBrowser.move(danmuX, danmuY) self.textPosDelta = self.textBrowser.pos() - videoPos def enterEvent(self, QEvent): self.hoverToken = True self.topLabel.show() self.frame.show() def leaveEvent(self, QEvent): self.hoverToken = False self.topLabel.hide() self.frame.hide() def doubleClick(self): if not self.top: # 非弹出类悬浮窗 self.popWindow.emit([ self.id, self.roomID, self.quality, True, self.startWithDanmu ]) self.mediaPlay(1, True) # 暂停播放 def leftMouseClicked(self): # 设置drag事件 发送拖动封面的房间号 drag = QDrag(self) mimeData = QMimeData() mimeData.setText('exchange:%s:%s' % (self.id, self.roomID)) drag.setMimeData(mimeData) drag.exec_() logging.debug('drag exchange:%s:%s' % (self.id, self.roomID)) def dragEnterEvent(self, QDragEnterEvent): QDragEnterEvent.accept() def dropEvent(self, QDropEvent): if QDropEvent.mimeData().hasText: text = QDropEvent.mimeData().text() # 拖拽事件 if 'roomID' in text: # 从cover拖拽新直播间 self.stopDanmuMessage() self.roomID = text.split(':')[1] self.addMedia.emit([self.id, self.roomID]) self.mediaReload() self.textBrowser.textBrowser.clear() self.textBrowser.transBrowser.clear() elif 'exchange' in text: # 交换窗口 fromID, fromRoomID = text.split(':')[1:] # exchange:id:roomID fromID = int(fromID) if fromID != self.id: self.exchangeMedia.emit( [fromID, fromRoomID, self.id, self.roomID]) def rightMouseClicked(self, event): menu = QMenu() exportCache = menu.addAction('导出视频缓存') openBrowser = menu.addAction('打开直播间') chooseQuality = menu.addMenu('选择画质 ►') originQuality = chooseQuality.addAction('原画') if self.quality == 10000: originQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) bluerayQuality = chooseQuality.addAction('蓝光') if self.quality == 400: bluerayQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) highQuality = chooseQuality.addAction('超清') if self.quality == 250: highQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) lowQuality = chooseQuality.addAction('流畅') if self.quality == 80: lowQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAudioChannel = menu.addMenu('选择音效 ►') chooseAudioOrigin = chooseAudioChannel.addAction('原始音效') if self.audioChannel == 0: chooseAudioOrigin.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAudioDolbys = chooseAudioChannel.addAction('杜比音效') if self.audioChannel == 5: chooseAudioDolbys.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmplify = menu.addMenu('音量增大 ►') chooseAmp_0_5 = chooseAmplify.addAction('x 0.5') if self.volumeAmplify == 0.5: chooseAmp_0_5.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_1 = chooseAmplify.addAction('x 1.0') if self.volumeAmplify == 1.0: chooseAmp_1.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_1_5 = chooseAmplify.addAction('x 1.5') if self.volumeAmplify == 1.5: chooseAmp_1_5.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_2 = chooseAmplify.addAction('x 2.0') if self.volumeAmplify == 2.0: chooseAmp_2.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_3 = chooseAmplify.addAction('x 3.0') if self.volumeAmplify == 3.0: chooseAmp_3.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_4 = chooseAmplify.addAction('x 4.0') if self.volumeAmplify == 4.0: chooseAmp_4.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) if not self.top: # 非弹出类悬浮窗 popWindow = menu.addAction('悬浮窗播放') else: opacityMenu = menu.addMenu('调节透明度 ►') percent100 = opacityMenu.addAction('100%') if self.opacity == 100: percent100.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent80 = opacityMenu.addAction('80%') if self.opacity == 80: percent80.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent60 = opacityMenu.addAction('60%') if self.opacity == 60: percent60.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent40 = opacityMenu.addAction('40%') if self.opacity == 40: percent40.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent20 = opacityMenu.addAction('20%') if self.opacity == 20: percent20.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) fullScreen = menu.addAction( '退出全屏') if self.isFullScreen() else menu.addAction('全屏') exit = menu.addAction('退出') action = menu.exec_(self.mapToGlobal(event.pos())) if action == exportCache: if self.cacheName and os.path.exists(self.cacheName): saveName = '%s_%s' % (self.uname, self.title) savePath = QFileDialog.getSaveFileName(self, "选择保存路径", saveName, "*.flv")[0] if savePath: # 保存路径有效 self.exportCache.setArgs(self.cacheName, savePath) self.exportCache.start() self.exportTip.setWindowTitle('导出缓存至%s' % savePath) self.exportTip.show() else: QMessageBox.information(self, '导出失败', '未检测到有效缓存\n%s' % self.cacheName, QMessageBox.Ok) elif action == openBrowser: if self.roomID != '0': QDesktopServices.openUrl( QUrl(r'https://live.bilibili.com/%s' % self.roomID)) elif action == originQuality: self.changeQuality.emit([self.id, 10000]) self.quality = 10000 self.mediaReload() elif action == bluerayQuality: self.changeQuality.emit([self.id, 400]) self.quality = 400 self.mediaReload() elif action == highQuality: self.changeQuality.emit([self.id, 250]) self.quality = 250 self.mediaReload() elif action == lowQuality: self.changeQuality.emit([self.id, 80]) self.quality = 80 self.mediaReload() elif action == chooseAudioOrigin: self.changeAudioChannel.emit([self.id, 0]) self.player.audio_set_channel(0) self.audioChannel = 0 elif action == chooseAudioDolbys: self.changeAudioChannel.emit([self.id, 5]) self.player.audio_set_channel(5) self.audioChannel = 5 elif action == chooseAmp_0_5: self.volumeAmplify = 0.5 elif action == chooseAmp_1: self.volumeAmplify = 1.0 elif action == chooseAmp_1_5: self.volumeAmplify = 1.5 elif action == chooseAmp_2: self.volumeAmplify = 2.0 elif action == chooseAmp_3: self.volumeAmplify = 3.0 elif action == chooseAmp_4: self.volumeAmplify = 4.0 if not self.top: if action == popWindow: self.popWindow.emit([ self.id, self.roomID, self.quality, False, self.startWithDanmu ]) self.mediaPlay(1, True) # 暂停播放 elif self.top: if action == percent100: self.setWindowOpacity(1) self.opacity = 100 elif action == percent80: self.setWindowOpacity(0.8) self.opacity = 80 elif action == percent60: self.setWindowOpacity(0.6) self.opacity = 60 elif action == percent40: self.setWindowOpacity(0.4) self.opacity = 40 elif action == percent20: self.setWindowOpacity(0.2) self.opacity = 20 elif action == fullScreen: if self.isFullScreen(): self.showNormal() else: self.showFullScreen() elif action == exit: self.hide() self.mediaStop() self.textBrowser.hide() def exportFinish(self, result): self.exportTip.hide() if result[0]: QMessageBox.information(self, '导出完成', result[1], QMessageBox.Ok) else: QMessageBox.information(self, '导出失败', result[1], QMessageBox.Ok) def setVolume(self, value): self.player.audio_set_volume(int(value * self.volumeAmplify)) self.volume = value # 记录volume值 每次刷新要用到 self.slider.setValue(value) self.volumeChanged.emit([self.id, value]) def closeDanmu(self): self.textSetting[0] = False # self.setDanmu.emit([self.id, False]) # 旧版信号 已弃用 # def closeTranslator(self): # self.setTranslator.emit([self.id, False]) def stopDanmuMessage(self): try: self.danmu.message.disconnect(self.playDanmu) except: pass self.danmu.terminate() def showDanmu(self): if self.textBrowser.isHidden(): self.textBrowser.show() if not self.startWithDanmu: self.danmu.message.connect(self.playDanmu) self.danmu.terminate() self.danmu.start() self.textSetting[0] = True self.startWithDanmu = True # self.translator.show() else: self.textBrowser.hide() # self.translator.hide() self.textSetting[0] = not self.textBrowser.isHidden() self.setDanmu.emit() # self.setTranslator.emit([self.id, not self.translator.isHidden()]) def mediaPlay(self, force=0, stopDownload=False): if force == 1: self.player.set_pause(1) self.userPause = True self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) elif force == 2: self.player.play() self.userPause = False self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) elif self.player.get_state() == vlc.State.Playing: self.player.set_pause(1) self.userPause = True self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) else: self.player.play() self.userPause = False self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) if stopDownload: self.getMediaURL.recordToken = False # 设置停止缓存标志位 self.getMediaURL.checkTimer.stop() self.checkPlaying.stop() def mediaMute(self, force=0, emit=True): if force == 1: self.player.audio_set_mute(False) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume)) elif force == 2: self.player.audio_set_mute(True) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolumeMuted)) elif self.player.audio_get_mute(): self.player.audio_set_mute(False) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume)) else: self.player.audio_set_mute(True) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolumeMuted)) if emit: self.mutedChanged.emit([self.id, self.player.audio_get_mute()]) def mediaReload(self): self.getMediaURL.recordToken = False # 设置停止缓存标志位 self.getMediaURL.checkTimer.stop() self.checkPlaying.stop() self.player.stop() if self.roomID != '0': self.setTitle() # 同时获取最新直播状态 if self.liveStatus == 1: # 直播中 self.getMediaURL.setConfig(self.roomID, self.quality) # 设置房号和画质 self.getMediaURL.start() # 开始缓存视频 self.getMediaURL.checkTimer.start(3000) # 启动监测定时器 self.checkPlaying.start(3000) # 启动播放卡顿检测定时器 else: self.mediaStop() def mediaStop(self): self.roomID = '0' self.topLabel.setText( (' 窗口%s 未定义的直播间' % (self.id + 1))[:20]) # 限制下直播间标题字数 self.titleLabel.setText('未定义') self.player.stop() self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.deleteMedia.emit(self.id) try: self.danmu.message.disconnect(self.playDanmu) except: pass self.getMediaURL.recordToken = False self.getMediaURL.checkTimer.stop() self.checkPlaying.stop() self.danmu.terminate() self.danmu.quit() self.danmu.wait() def setMedia(self, cacheName): self.cacheName = cacheName self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) self.danmu.setRoomID(self.roomID) try: self.danmu.message.disconnect(self.playDanmu) except: pass if self.startWithDanmu: self.danmu.message.connect(self.playDanmu) self.danmu.terminate() self.danmu.start() self.textBrowser.show() if self.hardwareDecode: self.media = self.instance.media_new( cacheName, 'avcodec-hw=dxva2') # 设置vlc并硬解播放 else: self.media = self.instance.media_new(cacheName) # 软解 self.player.set_media(self.media) # 设置视频 self.player.audio_set_channel(self.audioChannel) self.player.play() self.moveTimer.start() # 启动移动弹幕窗的timer def setTitle(self): if self.roomID == '0': self.title = '未定义的直播间' self.uname = '未定义' else: r = requests.get( r'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s' % self.roomID) data = json.loads(r.text) if data['message'] == '房间已加密': self.title = '房间已加密' self.uname = '房号: %s' % self.roomID elif not data['data']: self.title = '房间好像不见了-_-?' self.uname = '未定义' else: data = data['data'] self.liveStatus = data['room_info']['live_status'] self.title = data['room_info']['title'] self.uname = data['anchor_info']['base_info']['uname'] if self.liveStatus != 1: self.uname = '(未开播)' + self.uname self.topLabel.setText( (' 窗口%s %s' % (self.id + 1, self.title))[:20]) self.titleLabel.setText(self.uname) def playDanmu(self, message): token = False for symbol in self.filters: if symbol in message: self.textBrowser.transBrowser.append(message) # 同传不换行 token = True break if not token: self.textBrowser.textBrowser.append(message + '\n') def keyPressEvent(self, QKeyEvent): if QKeyEvent.key() == Qt.Key_Escape: if self.top and self.isFullScreen(): # 悬浮窗退出全屏 self.showNormal() else: self.fullScreenKey.emit() # 主界面退出全屏 elif QKeyEvent.key() == Qt.Key_H: self.hideBarKey.emit() elif QKeyEvent.key() == Qt.Key_F: self.fullScreenKey.emit() elif QKeyEvent.key() == Qt.Key_M: self.muteExceptKey.emit() # 这里调用self.id为啥是0???
class VideoWidget(QFrame): """ 视频播放窗口 """ mutedChanged = pyqtSignal(list) # 按下静音按钮 volumeChanged = pyqtSignal(list) # 音量滑条改变 addMedia = pyqtSignal(list) # 发送新增的直播 deleteMedia = pyqtSignal(int) # 删除选中的直播 exchangeMedia = pyqtSignal(list) # 交换播放窗口 setDanmu = pyqtSignal() # 发射弹幕设置信号 setTranslator = pyqtSignal(list) # 发送同传关闭信号 changeQuality = pyqtSignal(list) # 修改画质 changeAudioChannel = pyqtSignal(list) # 修改音效 popWindow = pyqtSignal(list) # 弹出悬浮窗 hideBarKey = pyqtSignal() # 隐藏控制条快捷键 fullScreenKey = pyqtSignal() # 全屏快捷键 muteExceptKey = pyqtSignal() # 除了这个播放器 其他全部静音快捷键 closePopWindow = pyqtSignal(list) # 关闭悬浮窗 def __init__(self, id, volume, cacheFolder, top=False, title='', resize=[], textSetting=[True, 20, 2, 6, 0, '【 [ {', 10], maxCacheSize=2048000, saveCachePath='', startWithDanmu=True, hardwareDecode=True): super(VideoWidget, self).__init__() self.setAcceptDrops(True) self.installEventFilter(self) self.id = id self.title = '' self.uname = '' self.oldTitle = '' self.oldUname = '' self.hoverToken = False self.roomID = '0' # 初始化直播间房号 self.liveStatus = 0 # 初始化直播状态为0 self.pauseToken = False self.quality = 250 self.audioChannel = 0 # 0 原始音效 5 杜比音效 self.volume = volume self.volumeAmplify = 1.0 # 音量加倍 self.muted = False self.hardwareDecode = hardwareDecode self.leftButtonPress = False self.rightButtonPress = False self.fullScreen = False self.userPause = False # 用户暂停 self.cacheName = '' self.maxCacheSize = maxCacheSize self.saveCachePath = saveCachePath self.startWithDanmu = startWithDanmu # 容器设置 self.setFrameShape(QFrame.Box) self.setObjectName('video') self.top = top self.name_str = f"悬浮窗{self.id}" if self.top else f"嵌入窗{self.id}" if top: self.setWindowFlags(Qt.Window) else: self.setStyleSheet( '#video{border-width:1px;border-style:solid;border-color:gray}' ) self.textSetting = textSetting self.horiPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][self.textSetting[2]] self.vertPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][self.textSetting[3]] self.filters = textSetting[5].split(' ') self.opacity = 100 if top: self.setWindowFlag(Qt.WindowStaysOnTopHint) if title: if top: self.setWindowTitle('%s %s' % (title, id + 1 - 9)) else: self.setWindowTitle('%s %s' % (title, id + 1)) layout = QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) # ---- 弹幕机 ---- self.textBrowser = TextBrowser( self) # 必须赶在resizeEvent和moveEvent之前初始化textbrowser self.setDanmuOpacity(self.textSetting[1]) # 设置弹幕透明度 self.textBrowser.optionWidget.opacitySlider.setValue( self.textSetting[1]) # 设置选项页透明条 self.textBrowser.optionWidget.opacitySlider.value.connect( self.setDanmuOpacity) self.setHorizontalPercent(self.textSetting[2]) # 设置横向占比 self.textBrowser.optionWidget.horizontalCombobox.setCurrentIndex( self.textSetting[2]) # 设置选项页占比框 self.textBrowser.optionWidget.horizontalCombobox.currentIndexChanged.connect( self.setHorizontalPercent) self.setVerticalPercent(self.textSetting[3]) # 设置横向占比 self.textBrowser.optionWidget.verticalCombobox.setCurrentIndex( self.textSetting[3]) # 设置选项页占比框 self.textBrowser.optionWidget.verticalCombobox.currentIndexChanged.connect( self.setVerticalPercent) self.setTranslateBrowser(self.textSetting[4]) self.textBrowser.optionWidget.translateCombobox.setCurrentIndex( self.textSetting[4]) # 设置同传窗口 self.textBrowser.optionWidget.translateCombobox.currentIndexChanged.connect( self.setTranslateBrowser) self.setTranslateFilter(self.textSetting[5]) # 同传过滤字符 self.textBrowser.optionWidget.translateFitler.setText( self.textSetting[5]) self.textBrowser.optionWidget.translateFitler.textChanged.connect( self.setTranslateFilter) self.setFontSize(self.textSetting[6]) # 设置弹幕字体大小 self.textBrowser.optionWidget.fontSizeCombox.setCurrentIndex( self.textSetting[6]) self.textBrowser.optionWidget.fontSizeCombox.currentIndexChanged.connect( self.setFontSize) self.textBrowser.closeSignal.connect(self.closeDanmu) self.textBrowser.moveSignal.connect(self.moveTextBrowser) if not self.startWithDanmu: # 如果启动隐藏被设置,隐藏弹幕机 self.textSetting[0] = False self.textBrowser.hide() self.textPosDelta = QPoint(0, 0) # 弹幕框和窗口之间的坐标差 self.deltaX = 0 self.deltaY = 0 # ---- 播放器布局设置 ---- # 播放器 self.videoFrame = VideoFrame() # 新版本vlc内核播放器 self.videoFrame.rightClicked.connect(self.rightMouseClicked) self.videoFrame.leftClicked.connect(self.leftMouseClicked) self.videoFrame.doubleClicked.connect(self.doubleClick) layout.addWidget(self.videoFrame, 0, 0, 12, 12) # vlc 实例 self.instance = vlc.Instance() self.newPlayer() # 实例化 player # 直播间标题 self.topLabel = QLabel() self.topLabel.setFixedHeight(30) # self.topLabel.setAlignment(Qt.AlignCenter) self.topLabel.setObjectName('frame') self.topLabel.setStyleSheet("background-color:#293038") # self.topLabel.setFixedHeight(32) self.topLabel.setFont(QFont('微软雅黑', 15, QFont.Bold)) layout.addWidget(self.topLabel, 0, 0, 1, 12) self.topLabel.hide() # 控制栏容器 self.frame = QWidget() self.frame.setObjectName('frame') self.frame.setStyleSheet("background-color:#293038") self.frame.setFixedHeight(50) frameLayout = QHBoxLayout(self.frame) frameLayout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.frame, 11, 0, 1, 12) self.frame.hide() # ---- 嵌入式播放器 控制栏 ---- # 主播名 self.titleLabel = QLabel() self.titleLabel.setMaximumWidth(150) self.titleLabel.setStyleSheet('background-color:#00000000') self.setTitle() frameLayout.addWidget(self.titleLabel) # 播放/暂停 self.play = PushButton(self.style().standardIcon(QStyle.SP_MediaPause)) self.play.clicked.connect(self.mediaPlay) frameLayout.addWidget(self.play) # 刷新 self.reload = PushButton(self.style().standardIcon( QStyle.SP_BrowserReload)) self.reload.clicked.connect(self.mediaReload) frameLayout.addWidget(self.reload) # 音量开关 self.volumeButton = PushButton(self.style().standardIcon( QStyle.SP_MediaVolume)) self.volumeButton.clicked.connect(self.mediaMute) frameLayout.addWidget(self.volumeButton) # 音量滑条 self.slider = Slider() self.slider.setStyleSheet('background-color:#00000000') self.slider.value.connect(self.setVolume) frameLayout.addWidget(self.slider) # 弹幕开关 self.danmuButton = PushButton(text='弹') self.danmuButton.clicked.connect(self.showDanmu) frameLayout.addWidget(self.danmuButton) # 关闭窗口 self.stop = PushButton(self.style().standardIcon( QStyle.SP_DialogCancelButton)) self.stop.clicked.connect(self.mediaStop) frameLayout.addWidget(self.stop) # ---- IO 交互设置 ---- # 单开线程获取视频流 self.getMediaURL = GetMediaURL(self.id, cacheFolder, maxCacheSize, saveCachePath) self.getMediaURL.cacheName.connect(self.setMedia) self.getMediaURL.copyFile.connect(self.copyCache) self.getMediaURL.downloadError.connect(self.mediaReload) self.danmu = remoteThread(self.roomID) # 导出配置 self.exportCache = ExportCache() self.exportCache.finish.connect(self.exportFinish) self.exportTip = ExportTip() # ---- 定时器 ---- # 弹幕机位置匹配 self.moveTimer = QTimer() self.moveTimer.timeout.connect(self.initTextPos) self.moveTimer.start(50) # 检查播放卡住的定时器 self.checkPlaying = QTimer() self.checkPlaying.timeout.connect(self.checkPlayStatus) # 最后再 resize 避免有变量尚未初始化 if resize: self.resize(resize[0], resize[1]) logging.info( f"{self.name_str} VLC 播放器构造完毕, 缓存大小: %dkb, 缓存路径: %s, 置顶?: %s, 启用弹幕?: %s" % (self.maxCacheSize, self.saveCachePath, self.top, self.startWithDanmu)) self.audioTimer = QTimer() self.audioTimer.timeout.connect(self.checkAudio) self.audioTimer.setInterval(100) def checkPlayStatus(self): # 播放卡住了 if not self.player.is_playing() and not self.isHidden( ) and self.liveStatus != 0 and not self.userPause: self.retryTimes += 1 if self.retryTimes > 10: # 10秒内未刷新 self.mediaReload() # 彻底刷新 else: # self.player.pause() # 不完全刷新 self.player.stop() self.player.release() self.player = self.instance.media_player_new() self.player.video_set_mouse_input(False) self.player.video_set_key_input(False) if platform.system() == 'Windows': self.player.set_hwnd(self.videoFrame.winId()) elif platform.system() == 'Darwin': # for MacOS self.player.set_nsobject(int(self.videoFrame.winId())) else: self.player.set_xwindow(self.videoFrame.winId()) if self.hardwareDecode: self.media = self.instance.media_new( self.cacheName, 'avcodec-hw=dxva2') # 设置vlc并硬解播放 else: self.media = self.instance.media_new(self.cacheName) # 软解 self.player.set_media(self.media) # 设置视频 self.player.audio_set_channel(self.audioChannel) self.player.play() self.audioTimer.stop() self.audioTimer.start() # 检测音量 def checkAudio(self): volume = int(self.volume * self.volumeAmplify) if self.player.audio_get_volume() != volume: self.player.audio_set_volume(volume) elif self.player.audio_get_mute != self.muted: self.player.audio_set_mute(self.muted) else: self.audioTimer.stop() def initTextPos(self): # 初始化弹幕机位置 videoPos = self.mapToGlobal(self.videoFrame.pos()) if self.textBrowser.pos() != videoPos: self.textBrowser.move(videoPos) else: self.moveTimer.stop() def setDanmuOpacity(self, value): if value < 7: value = 7 # 最小透明度 self.textSetting[1] = value # 记录设置 value = int(value / 101 * 256) color = str(hex(value))[2:] + '000000' self.textBrowser.textBrowser.setStyleSheet('background-color:#%s' % color) self.textBrowser.transBrowser.setStyleSheet('background-color:#%s' % color) self.setDanmu.emit() def setHorizontalPercent(self, index): # 设置弹幕框水平宽度 self.textSetting[2] = index self.horiPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][index] # 记录横向占比 width = self.width() * self.horiPercent self.textBrowser.resize(width, self.textBrowser.height()) # if width > 240: # self.textBrowser.textBrowser.setFont(QFont('Microsoft JhengHei', 17, QFont.Bold)) # self.textBrowser.transBrowser.setFont(QFont('Microsoft JhengHei', 17, QFont.Bold)) # elif 100 < width <= 240: # self.textBrowser.textBrowser.setFont(QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) # self.textBrowser.transBrowser.setFont(QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) # else: # self.textBrowser.textBrowser.setFont(QFont('Microsoft JhengHei', 10, QFont.Bold)) # self.textBrowser.transBrowser.setFont(QFont('Microsoft JhengHei', 10, QFont.Bold)) self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) self.setDanmu.emit() def setVerticalPercent(self, index): # 设置弹幕框垂直高度 self.textSetting[3] = index self.vertPercent = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][index] # 记录纵向占比 self.textBrowser.resize(self.textBrowser.width(), self.height() * self.vertPercent) self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) self.setDanmu.emit() def setTranslateBrowser(self, index): self.textSetting[4] = index if index == 0: # 显示弹幕和同传 self.textBrowser.textBrowser.show() self.textBrowser.transBrowser.show() elif index == 1: # 只显示弹幕 self.textBrowser.transBrowser.hide() self.textBrowser.textBrowser.show() elif index == 2: # 只显示同传 self.textBrowser.textBrowser.hide() self.textBrowser.transBrowser.show() self.textBrowser.resize(self.width() * self.horiPercent, self.height() * self.vertPercent) self.setDanmu.emit() def setTranslateFilter(self, filterWords): self.textSetting[5] = filterWords self.filters = filterWords.split(' ') self.setDanmu.emit() def setFontSize(self, index): self.textSetting[6] = index self.textBrowser.textBrowser.setFont( QFont('Microsoft JhengHei', index + 5, QFont.Bold)) self.textBrowser.transBrowser.setFont( QFont('Microsoft JhengHei', index + 5, QFont.Bold)) self.setDanmu.emit() def resizeEvent(self, QEvent): self.titleLabel.hide() if self.width() < 350 else self.titleLabel.show( ) self.play.hide() if self.width() < 300 else self.play.show() self.danmuButton.hide( ) if self.width() < 250 else self.danmuButton.show() self.slider.hide() if self.width() < 200 else self.slider.show() width = self.width() * self.horiPercent self.textBrowser.resize(width, self.height() * self.vertPercent) self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) self.moveTextBrowser() def moveEvent( self, QMoveEvent): # 理论上给悬浮窗同步弹幕机用的moveEvent 但不生效 但是又不能删掉 不然交换窗口弹幕机有bug videoPos = self.mapToGlobal( self.videoFrame.pos()) # videoFrame的坐标要转成globalPos self.textBrowser.move(videoPos + self.textPosDelta) def moveTextBrowser(self, point=None): videoPos = self.mapToGlobal( self.videoFrame.pos()) # videoFrame的坐标要转成globalPos if point: danmuX, danmuY = point.x(), point.y() else: danmuX, danmuY = self.textBrowser.x(), self.textBrowser.y( ) # textBrowser坐标本身就是globalPos videoX, videoY = videoPos.x(), videoPos.y() videoW, videoH = self.videoFrame.width(), self.videoFrame.height() danmuW, danmuH = self.textBrowser.width(), self.textBrowser.height() smaller = False # 弹幕机尺寸大于播放窗 if danmuW > videoW or danmuH > videoH + 5: # +5是为了在100%纵向的时候可以左右拖 有的屏幕计算会略微超过一两个像素 danmuX, danmuY = videoX, videoY smaller = True if not smaller: if danmuX < videoX: danmuX = videoX elif danmuX > videoX + videoW - danmuW: danmuX = videoX + videoW - danmuW if danmuY < videoY: danmuY = videoY elif danmuY > videoY + videoH - danmuH: danmuY = videoY + videoH - danmuH self.textBrowser.move(danmuX, danmuY) self.textPosDelta = self.textBrowser.pos() - videoPos self.deltaX, self.deltaY = self.textPosDelta.x() / self.width( ), self.textPosDelta.y() / self.height() def enterEvent(self, QEvent): self.hoverToken = True self.topLabel.show() self.frame.show() def leaveEvent(self, QEvent): self.hoverToken = False self.topLabel.hide() self.frame.hide() def doubleClick(self): if not self.top: # 非弹出类悬浮窗 self.popWindow.emit([ self.id, self.roomID, self.quality, True, self.startWithDanmu ]) self.mediaStop() # 直接停止播放原窗口 def leftMouseClicked(self): # 设置drag事件 发送拖动封面的房间号 drag = QDrag(self) mimeData = QMimeData() if self.top: # 悬浮窗 # mimeData.setText('roomID:%s' % self.roomID) mimeData.setText('') else: mimeData.setText('exchange:%s:%s' % (self.id, self.roomID)) drag.setMimeData(mimeData) drag.exec_() logging.debug(f'{self.name_str} drag exchange:%s:%s' % (self.id, self.roomID)) def dragEnterEvent(self, QDragEnterEvent): QDragEnterEvent.accept() def dropEvent(self, QDropEvent): if QDropEvent.mimeData().hasText: text = QDropEvent.mimeData().text() # 拖拽事件 print(text) if 'roomID' in text: # 从cover拖拽新直播间 self.stopDanmuMessage() self.roomID = text.split(':')[1] self.addMedia.emit([self.id, self.roomID]) self.mediaReload() self.textBrowser.textBrowser.clear() self.textBrowser.transBrowser.clear() elif 'exchange' in text: # 交换窗口 fromID, fromRoomID = text.split(':')[1:] # exchange:id:roomID fromID = int(fromID) print(fromID, self.id) if fromID != self.id: self.exchangeMedia.emit( [fromID, fromRoomID, self.id, self.roomID]) def rightMouseClicked(self, event): menu = QMenu() exportCache = menu.addAction('导出视频缓存') openBrowser = menu.addAction('打开直播间') chooseQuality = menu.addMenu('选择画质 ►') originQuality = chooseQuality.addAction('原画') if self.quality == 10000: originQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) bluerayQuality = chooseQuality.addAction('蓝光') if self.quality == 400: bluerayQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) highQuality = chooseQuality.addAction('超清') if self.quality == 250: highQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) lowQuality = chooseQuality.addAction('流畅') if self.quality == 80: lowQuality.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAudioChannel = menu.addMenu('选择音效 ►') chooseAudioOrigin = chooseAudioChannel.addAction('原始音效') if self.audioChannel == 0: chooseAudioOrigin.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAudioDolbys = chooseAudioChannel.addAction('杜比音效') if self.audioChannel == 5: chooseAudioDolbys.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmplify = menu.addMenu('音量增大 ►') chooseAmp_0_5 = chooseAmplify.addAction('x 0.5') if self.volumeAmplify == 0.5: chooseAmp_0_5.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_1 = chooseAmplify.addAction('x 1.0') if self.volumeAmplify == 1.0: chooseAmp_1.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_1_5 = chooseAmplify.addAction('x 1.5') if self.volumeAmplify == 1.5: chooseAmp_1_5.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_2 = chooseAmplify.addAction('x 2.0') if self.volumeAmplify == 2.0: chooseAmp_2.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_3 = chooseAmplify.addAction('x 3.0') if self.volumeAmplify == 3.0: chooseAmp_3.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) chooseAmp_4 = chooseAmplify.addAction('x 4.0') if self.volumeAmplify == 4.0: chooseAmp_4.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) if not self.top: # 非弹出类悬浮窗 popWindow = menu.addAction('悬浮窗播放') else: # 弹出的悬浮窗 opacityMenu = menu.addMenu('调节透明度 ►') percent100 = opacityMenu.addAction('100%') if self.opacity == 100: percent100.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent80 = opacityMenu.addAction('80%') if self.opacity == 80: percent80.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent60 = opacityMenu.addAction('60%') if self.opacity == 60: percent60.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent40 = opacityMenu.addAction('40%') if self.opacity == 40: percent40.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) percent20 = opacityMenu.addAction('20%') if self.opacity == 20: percent20.setIcon(self.style().standardIcon( QStyle.SP_DialogApplyButton)) fullScreen = menu.addAction( '退出全屏') if self.isFullScreen() else menu.addAction('全屏') exit = menu.addAction('退出') action = menu.exec_(self.mapToGlobal(event.pos())) if action == exportCache: if self.cacheName and os.path.exists(self.cacheName): saveName = '%s_%s' % (self.uname, self.title) savePath = QFileDialog.getSaveFileName(self, "选择保存路径", saveName, "*.flv")[0] if savePath: # 保存路径有效 self.exportCache.setArgs(self.cacheName, savePath) self.exportCache.start() self.exportTip.setWindowTitle('导出缓存至%s' % savePath) self.exportTip.show() else: QMessageBox.information(self, '导出失败', '未检测到有效缓存\n%s' % self.cacheName, QMessageBox.Ok) elif action == openBrowser: if self.roomID != '0': QDesktopServices.openUrl( QUrl(r'https://live.bilibili.com/%s' % self.roomID)) elif action == originQuality: self.changeQuality.emit([self.id, 10000]) self.quality = 10000 self.mediaReload() elif action == bluerayQuality: self.changeQuality.emit([self.id, 400]) self.quality = 400 self.mediaReload() elif action == highQuality: self.changeQuality.emit([self.id, 250]) self.quality = 250 self.mediaReload() elif action == lowQuality: self.changeQuality.emit([self.id, 80]) self.quality = 80 self.mediaReload() elif action == chooseAudioOrigin: self.changeAudioChannel.emit([self.id, 0]) self.player.audio_set_channel(0) self.audioChannel = 0 elif action == chooseAudioDolbys: self.changeAudioChannel.emit([self.id, 5]) self.player.audio_set_channel(5) self.audioChannel = 5 elif action == chooseAmp_0_5: self.volumeAmplify = 0.5 self.audioTimer.start() elif action == chooseAmp_1: self.volumeAmplify = 1.0 self.audioTimer.start() elif action == chooseAmp_1_5: self.volumeAmplify = 1.5 self.audioTimer.start() elif action == chooseAmp_2: self.volumeAmplify = 2.0 self.audioTimer.start() elif action == chooseAmp_3: self.volumeAmplify = 3.0 self.audioTimer.start() elif action == chooseAmp_4: self.volumeAmplify = 4.0 self.audioTimer.start() if not self.top: if action == popWindow: self.popWindow.emit([ self.id, self.roomID, self.quality, False, self.startWithDanmu ]) self.mediaStop() # 停止播放 # self.mediaPlay(1, True) # 暂停播放 elif self.top: if action == percent100: self.setWindowOpacity(1) self.opacity = 100 elif action == percent80: self.setWindowOpacity(0.8) self.opacity = 80 elif action == percent60: self.setWindowOpacity(0.6) self.opacity = 60 elif action == percent40: self.setWindowOpacity(0.4) self.opacity = 40 elif action == percent20: self.setWindowOpacity(0.2) self.opacity = 20 elif action == fullScreen: if self.isFullScreen(): self.showNormal() else: self.showFullScreen() elif action == exit: if self.top: self.closePopWindow.emit([self.id, self.roomID]) self.hide() self.mediaStop() self.textBrowser.hide() def closeEvent(self, event): """拦截关闭按钮事件,隐藏弹出的悬浮窗 修改务必同步右键菜单的退出事件:"action == exit" """ event.ignore() # 忽略关闭事件 if self.top: self.closePopWindow.emit([self.id, self.roomID]) self.hide() self.mediaStop() self.textBrowser.hide() logging.debug(f"{self.name_str}隐藏") def exportFinish(self, result): self.exportTip.hide() if result[0]: QMessageBox.information(self, '导出完成', result[1], QMessageBox.Ok) else: QMessageBox.information(self, '导出失败', result[1], QMessageBox.Ok) def setVolume(self, value): self.player.audio_set_volume(int(value * self.volumeAmplify)) self.volume = value # 记录volume值 每次刷新要用到 self.slider.setValue(value) self.volumeChanged.emit([self.id, value]) def closeDanmu(self): self.textSetting[0] = False # self.setDanmu.emit([self.id, False]) # 旧版信号 已弃用 # def closeTranslator(self): # self.setTranslator.emit([self.id, False]) def stopDanmuMessage(self): try: self.danmu.message.disconnect(self.playDanmu) except: logging.exception('停止弹幕出错') self.danmu.terminate() def showDanmu(self): if self.textBrowser.isHidden(): self.textBrowser.show() if not self.startWithDanmu: self.danmu.message.connect(self.playDanmu) self.danmu.terminate() self.danmu.start() self.textSetting[0] = True self.startWithDanmu = True else: self.textBrowser.hide() self.startWithDanmu = False self.textSetting[0] = not self.textBrowser.isHidden() self.setDanmu.emit() def mediaPlay(self, force=0, stopDownload=False, setUserPause=False): if force == 1: self.player.set_pause(1) if setUserPause: self.userPause = True self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) elif force == 2: self.player.play() if setUserPause: self.userPause = False self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) elif self.player.get_state() == vlc.State.Playing: self.player.set_pause(1) self.userPause = True self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) else: self.player.play() self.userPause = False self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) if stopDownload: self.getMediaURL.recordToken = False # 设置停止缓存标志位 self.getMediaURL.checkTimer.stop() self.checkPlaying.stop() logging.debug(f"{self.name_str}按下暂停/播放键") def mediaMute(self, force=0, emit=True): logging.debug(f"{self.name_str}按下音量开关键") logging.debug(f" force={force}, emit={emit}") voice_str = "音量Off" if self.player.audio_get_mute() else "音量On" logging.debug(f" bgein mute status={voice_str}") if force == 1: self.muted = False # self.player.audio_set_mute(False) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume)) elif force == 2: self.muted = True # self.player.audio_set_mute(True) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolumeMuted)) elif self.player.audio_get_mute(): self.muted = False # self.player.audio_set_mute(False) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume)) else: self.muted = True # self.player.audio_set_mute(True) self.volumeButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolumeMuted)) self.audioTimer.start() if emit: self.mutedChanged.emit([self.id, self.muted]) voice_str = "音量Off" if self.player.audio_get_mute() else "音量On" logging.debug(f" final mute status={voice_str}") def mediaReload(self): self.getMediaURL.recordToken = False # 设置停止缓存标志位 self.getMediaURL.checkTimer.stop() self.checkPlaying.stop() if self.roomID != '0': self.playerRestart() self.setTitle() # 同时获取最新直播状态 if self.liveStatus == 1: # 直播中 self.getMediaURL.setConfig(self.roomID, self.quality) # 设置房号和画质 self.getMediaURL.start() # 开始缓存视频 self.getMediaURL.checkTimer.start(3000) # 启动监测定时器 else: self.mediaStop() def mediaStop(self, deleteMedia=True): # self.userPause = True self.oldTitle, self.oldUname = '', '' self.roomID = '0' self.topLabel.setText( (' 窗口%s 未定义的直播间' % (self.id + 1))[:20]) # 限制下直播间标题字数 self.titleLabel.setText('未定义') self.playerRestart() self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) if deleteMedia: self.deleteMedia.emit(self.id) try: self.danmu.message.disconnect(self.playDanmu) except: logging.exception('停止弹幕出错') self.getMediaURL.recordToken = False self.getMediaURL.checkTimer.stop() self.checkPlaying.stop() self.danmu.terminate() self.danmu.quit() self.danmu.wait() def setMedia(self, cacheName): self.retryTimes = 0 self.cacheName = cacheName self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) self.danmu.setRoomID(self.roomID) try: self.danmu.message.disconnect(self.playDanmu) except: logging.exception('停止弹幕出错') if self.startWithDanmu: self.danmu.message.connect(self.playDanmu) self.danmu.terminate() self.danmu.start() self.textBrowser.show() if self.hardwareDecode: self.media = self.instance.media_new( cacheName, 'avcodec-hw=any') # 设置vlc并硬解播放 else: self.media = self.instance.media_new( cacheName, 'avcodec-hw=none') # 软解 vlc3.0似乎不起作用? self.player.set_media(self.media) # 设置视频 self.player.audio_set_channel(self.audioChannel) self.player.play() # self.moveTimer.start() # 启动移动弹幕窗的timer self.checkPlaying.start(1000) # 启动播放卡顿检测定时器 self.audioTimer.start() # 检测音量是否正确 def copyCache(self, copyFile): title = self.oldTitle if self.oldTitle else self.title for s in ['/', '\\', ':', '*', '"', '<', '>', '|', '?']: title = title.replace(s, '') uname = self.oldUname if self.oldUname else self.uname formatTime = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time())) self.exportCache.setArgs( copyFile, '%s/%s_%s_%s.flv' % (self.saveCachePath, uname, title, formatTime)) self.exportCache.cut = True # 设置为剪切 self.exportCache.start() """==== self.player 相关函数 ==== + newPlayer() 新实例化一个 self.player。初始化用 + playerRestart() 重置 self.player + playerFree() 释放并销毁 playerFree 实例 """ def newPlayer(self): """实例化 player 依赖实例化的 vlc (self.instance) """ self.player = self.instance.media_player_new() # 视频播放 self.player.video_set_mouse_input(False) self.player.video_set_key_input(False) # 将播放器实例绑定到 VideoFrame: QFrame if platform.system() == 'Windows': self.player.set_hwnd(self.videoFrame.winId()) elif platform.system() == 'Darwin': # for MacOS self.player.set_nsobject(int(self.videoFrame.winId())) else: self.player.set_xwindow(self.videoFrame.winId()) def playerRestart(self): """重置 player vlc 实例(self.instance)保持不动 """ if self.player: self.player.stop() self.player.release() self.newPlayer() # 后续视频流设置由 GetMediaURL 发送 cacheName 信号执行 self.setMedia 完成 def playerFree(self): """销毁 player 实例。退出主程序前调用""" if self.player: self.player.stop() self.player.release() def setTitle(self): if self.title != '未定义的直播间': self.oldTitle = self.title if self.uname != '未定义': self.oldUname = self.uname if self.roomID == '0': self.title = '未定义的直播间' self.uname = '未定义' else: r = requests.get( r'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s' % self.roomID) data = json.loads(r.text) if data['message'] == '房间已加密': self.title = '房间已加密' self.uname = '房号: %s' % self.roomID elif not data['data']: self.title = '房间好像不见了-_-?' self.uname = '未定义' else: data = data['data'] self.liveStatus = data['room_info']['live_status'] self.title = data['room_info']['title'] self.uname = data['anchor_info']['base_info']['uname'] if self.liveStatus != 1: self.uname = '(未开播)' + self.uname self.topLabel.setText( (' 窗口%s %s' % (self.id + 1, self.title))[:20]) self.titleLabel.setText(self.uname) def playDanmu(self, message): token = False for symbol in self.filters: if symbol in message: self.textBrowser.transBrowser.append(message) # 同传不换行 token = True break if not token: self.textBrowser.textBrowser.append(message + '\n') def keyPressEvent(self, QKeyEvent): if QKeyEvent.key() == Qt.Key_Escape: if self.top and self.isFullScreen(): # 悬浮窗退出全屏 self.showNormal() else: self.fullScreenKey.emit() # 主界面退出全屏 elif QKeyEvent.key() == Qt.Key_H: self.hideBarKey.emit() elif QKeyEvent.key() == Qt.Key_F: self.fullScreenKey.emit() elif QKeyEvent.key() == Qt.Key_M or QKeyEvent.key() == Qt.Key_S: self.muteExceptKey.emit() # 这里调用self.id为啥是0???