forked from openlp/openlp
Made it possible to name optical clips, and fixed some issues.
This commit is contained in:
parent
06825e4aeb
commit
2a1a93d319
@ -110,6 +110,7 @@ def set_media_players(players_list, overridden_player='auto'):
|
|||||||
players = players.replace(overridden_player, '[%s]' % overridden_player)
|
players = players.replace(overridden_player, '[%s]' % overridden_player)
|
||||||
Settings().setValue('media/players', players)
|
Settings().setValue('media/players', players)
|
||||||
|
|
||||||
|
|
||||||
def parse_optical_path(input):
|
def parse_optical_path(input):
|
||||||
"""
|
"""
|
||||||
Split the optical path info.
|
Split the optical path info.
|
||||||
@ -123,10 +124,24 @@ def parse_optical_path(input):
|
|||||||
subtitle_track = int(clip_info[3])
|
subtitle_track = int(clip_info[3])
|
||||||
start = float(clip_info[4])
|
start = float(clip_info[4])
|
||||||
end = float(clip_info[5])
|
end = float(clip_info[5])
|
||||||
filename = clip_info[6]
|
clip_name = clip_info[6]
|
||||||
if len(clip_info) > 7:
|
filename = clip_info[7]
|
||||||
filename += clip_info[7]
|
# Windows path usually contains a colon after the drive letter
|
||||||
return filename, title, audio_track, subtitle_track, start, end
|
if len(clip_info) > 8:
|
||||||
|
filename += clip_info[8]
|
||||||
|
return filename, title, audio_track, subtitle_track, start, end, clip_name
|
||||||
|
|
||||||
|
|
||||||
|
def format_milliseconds(milliseconds):
|
||||||
|
"""
|
||||||
|
Format milliseconds into a human readable time string.
|
||||||
|
:param milliseconds: Milliseconds to format
|
||||||
|
:return: Time string in format: hh.mm.ss,ttt
|
||||||
|
"""
|
||||||
|
seconds, millis = divmod(milliseconds, 1000)
|
||||||
|
minutes, seconds = divmod(seconds, 60)
|
||||||
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis)
|
||||||
|
|
||||||
from .mediacontroller import MediaController
|
from .mediacontroller import MediaController
|
||||||
from .playertab import PlayerTab
|
from .playertab import PlayerTab
|
||||||
|
@ -373,7 +373,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
|||||||
if service_item.is_capable(ItemCapabilities.IsOptical):
|
if service_item.is_capable(ItemCapabilities.IsOptical):
|
||||||
log.debug('video is optical and live')
|
log.debug('video is optical and live')
|
||||||
path = service_item.get_frame_path()
|
path = service_item.get_frame_path()
|
||||||
(name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path)
|
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
|
||||||
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
|
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
|
||||||
controller)
|
controller)
|
||||||
else:
|
else:
|
||||||
@ -392,7 +392,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
|||||||
if service_item.is_capable(ItemCapabilities.IsOptical):
|
if service_item.is_capable(ItemCapabilities.IsOptical):
|
||||||
log.debug('video is optical and preview')
|
log.debug('video is optical and preview')
|
||||||
path = service_item.get_frame_path()
|
path = service_item.get_frame_path()
|
||||||
(name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path)
|
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
|
||||||
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
|
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
|
||||||
controller)
|
controller)
|
||||||
else:
|
else:
|
||||||
@ -456,6 +456,19 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller):
|
def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller):
|
||||||
|
"""
|
||||||
|
Setup playback of optical media
|
||||||
|
|
||||||
|
:param filename: Path of the optical device/drive.
|
||||||
|
:param title: The main/title track to play.
|
||||||
|
:param audio_track: The audio track to play.
|
||||||
|
:param subtitle_track: The subtitle track to play.
|
||||||
|
:param start: Start position in miliseconds.
|
||||||
|
:param end: End position in miliseconds.
|
||||||
|
:param display: The display to play the media.
|
||||||
|
:param controller: The media contraoller.
|
||||||
|
:return: True if setup succeded else False.
|
||||||
|
"""
|
||||||
log.debug('media_setup_optical')
|
log.debug('media_setup_optical')
|
||||||
if controller is None:
|
if controller is None:
|
||||||
controller = self.display_controllers[DisplayControllerType.Plugin]
|
controller = self.display_controllers[DisplayControllerType.Plugin]
|
||||||
|
@ -43,6 +43,7 @@ from PyQt4 import QtCore, QtGui
|
|||||||
from openlp.core.common import translate
|
from openlp.core.common import translate
|
||||||
from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector
|
from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
|
from openlp.core.ui.media import format_milliseconds
|
||||||
try:
|
try:
|
||||||
from openlp.core.ui.media.vendor import vlc
|
from openlp.core.ui.media.vendor import vlc
|
||||||
except (ImportError, NameError, NotImplementedError):
|
except (ImportError, NameError, NotImplementedError):
|
||||||
@ -156,7 +157,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
def detect_audio_cd(self, path):
|
def detect_audio_cd(self, path):
|
||||||
"""
|
"""
|
||||||
Detects is the given path is an audio CD
|
Detects is the given path is an audio CD
|
||||||
|
|
||||||
:param path: Path to the device to be tested.
|
:param path: Path to the device to be tested.
|
||||||
:return: True if it was an audio CD else False.
|
:return: True if it was an audio CD else False.
|
||||||
"""
|
"""
|
||||||
@ -166,7 +167,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
self.vlc_media_player.play()
|
self.vlc_media_player.play()
|
||||||
# Wait for media to start playing. In this case VLC actually returns an error.
|
# Wait for media to start playing. In this case VLC actually returns an error.
|
||||||
self.media_state_wait(vlc.State.Playing)
|
self.media_state_wait(vlc.State.Playing)
|
||||||
self.vlc_media_player.pause()
|
self.vlc_media_player.set_pause(1)
|
||||||
# If subitems exists, this is a CD
|
# If subitems exists, this is a CD
|
||||||
self.audio_cd_tracks = self.vlc_media.subitems()
|
self.audio_cd_tracks = self.vlc_media.subitems()
|
||||||
if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1:
|
if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1:
|
||||||
@ -231,7 +232,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
'VLC player failed playing the media'))
|
'VLC player failed playing the media'))
|
||||||
self.toggle_disable_load_media(False)
|
self.toggle_disable_load_media(False)
|
||||||
return
|
return
|
||||||
self.vlc_media_player.pause()
|
self.vlc_media_player.set_pause(1)
|
||||||
self.vlc_media_player.set_time(0)
|
self.vlc_media_player.set_time(0)
|
||||||
if not self.audio_cd:
|
if not self.audio_cd:
|
||||||
# Get titles, insert in combobox
|
# Get titles, insert in combobox
|
||||||
@ -247,6 +248,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
# Enable audio track combobox if anything is in it
|
# Enable audio track combobox if anything is in it
|
||||||
if len(titles) > 0:
|
if len(titles) > 0:
|
||||||
self.title_combo_box.setDisabled(False)
|
self.title_combo_box.setDisabled(False)
|
||||||
|
self.vlc_media_player.set_pause(1)
|
||||||
self.toggle_disable_load_media(False)
|
self.toggle_disable_load_media(False)
|
||||||
|
|
||||||
@QtCore.pyqtSlot(bool)
|
@QtCore.pyqtSlot(bool)
|
||||||
@ -367,7 +369,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
if not self.media_state_wait(vlc.State.Playing):
|
if not self.media_state_wait(vlc.State.Playing):
|
||||||
return
|
return
|
||||||
# pause
|
# pause
|
||||||
self.vlc_media_player.pause()
|
self.vlc_media_player.set_pause(1)
|
||||||
self.vlc_media_player.set_time(0)
|
self.vlc_media_player.set_time(0)
|
||||||
self.vlc_media_player.audio_set_mute(False)
|
self.vlc_media_player.audio_set_mute(False)
|
||||||
self.toggle_disable_player(False)
|
self.toggle_disable_player(False)
|
||||||
@ -379,7 +381,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
if not self.media_state_wait(vlc.State.Playing):
|
if not self.media_state_wait(vlc.State.Playing):
|
||||||
return
|
return
|
||||||
# pause
|
# pause
|
||||||
self.vlc_media_player.pause()
|
self.vlc_media_player.set_pause(1)
|
||||||
self.vlc_media_player.set_time(0)
|
self.vlc_media_player.set_time(0)
|
||||||
# Get audio tracks, insert in combobox
|
# Get audio tracks, insert in combobox
|
||||||
audio_tracks = self.vlc_media_player.audio_get_track_description()
|
audio_tracks = self.vlc_media_player.audio_get_track_description()
|
||||||
@ -520,12 +522,38 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
path = self.media_path_combobox.currentText()
|
path = self.media_path_combobox.currentText()
|
||||||
optical = ''
|
optical = ''
|
||||||
if self.audio_cd:
|
if self.audio_cd:
|
||||||
optical = 'optical:' + str(title) + ':-1:-1:' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + path
|
optical = 'optical:' + str(title) + ':-1:-1:' + str(start_time_ms) + ':' + str(end_time_ms) + ':'
|
||||||
else:
|
else:
|
||||||
audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex())
|
audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex())
|
||||||
subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex())
|
subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex())
|
||||||
optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str(
|
optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str(
|
||||||
start_time_ms) + ':' + str(end_time_ms) + ':' + path
|
start_time_ms) + ':' + str(end_time_ms) + ':'
|
||||||
|
# Ask for an alternative name for the mediaclip
|
||||||
|
while True:
|
||||||
|
new_optical_name, ok = QtGui.QInputDialog.getText(self, translate('MediaPlugin.MediaClipSelectorForm',
|
||||||
|
'Set name of mediaclip'),
|
||||||
|
translate('MediaPlugin.MediaClipSelectorForm',
|
||||||
|
'Name of mediaclip:'),
|
||||||
|
QtGui.QLineEdit.Normal)
|
||||||
|
# User pressed cancel, don't save the clip
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
# User pressed ok, but the input text is blank
|
||||||
|
if not new_optical_name:
|
||||||
|
critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm',
|
||||||
|
'Enter a valid name or cancel'),
|
||||||
|
translate('MediaPlugin.MediaClipSelectorForm',
|
||||||
|
'Enter a valid name or cancel'))
|
||||||
|
# The entered new name contains a colon, which we don't allow because colons is used to seperate clip info
|
||||||
|
elif new_optical_name.find(':') >= 0:
|
||||||
|
critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'Invalid character'),
|
||||||
|
translate('MediaPlugin.MediaClipSelectorForm',
|
||||||
|
'The name of the mediaclip must not contain the character ":"'))
|
||||||
|
# New name entered and we use it
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
# Append the new name to the optical string and the path
|
||||||
|
optical += new_optical_name + ':' + path
|
||||||
self.media_item.add_optical_clip(optical)
|
self.media_item.add_optical_clip(optical)
|
||||||
|
|
||||||
def media_state_wait(self, media_state):
|
def media_state_wait(self, media_state):
|
||||||
@ -585,7 +613,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
log.debug('could not use udisks, will try udisks2')
|
log.debug('could not use udisks, will try udisks2')
|
||||||
udev_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2')
|
udev_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2')
|
||||||
udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.DBus.ObjectManager')
|
udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.DBus.ObjectManager')
|
||||||
for k,v in udev_manager.GetManagedObjects().items():
|
for k, v in udev_manager.GetManagedObjects().items():
|
||||||
drive_info = v.get('org.freedesktop.UDisks2.Drive', {})
|
drive_info = v.get('org.freedesktop.UDisks2.Drive', {})
|
||||||
drive_props = drive_info.get('MediaCompatibility')
|
drive_props = drive_info.get('MediaCompatibility')
|
||||||
if drive_props and any('optical' in prop for prop in drive_props):
|
if drive_props and any('optical' in prop for prop in drive_props):
|
||||||
@ -593,7 +621,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
if dbus.String('org.freedesktop.UDisks2.Block') in device:
|
if dbus.String('org.freedesktop.UDisks2.Block') in device:
|
||||||
if device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('Drive')] == k:
|
if device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('Drive')] == k:
|
||||||
block_file = ''
|
block_file = ''
|
||||||
for c in device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('PreferredDevice')]:
|
for c in device[dbus.String('org.freedesktop.UDisks2.Block')][
|
||||||
|
dbus.String('PreferredDevice')]:
|
||||||
if chr(c) != '\x00':
|
if chr(c) != '\x00':
|
||||||
block_file += chr(c)
|
block_file += chr(c)
|
||||||
self.media_path_combobox.addItem(block_file)
|
self.media_path_combobox.addItem(block_file)
|
||||||
@ -605,5 +634,12 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
|
|||||||
if volume.startswith('.'):
|
if volume.startswith('.'):
|
||||||
continue
|
continue
|
||||||
dirs = os.listdir('/Volumes/' + volume)
|
dirs = os.listdir('/Volumes/' + volume)
|
||||||
|
# Detect DVD
|
||||||
if 'VIDEO_TS' in dirs:
|
if 'VIDEO_TS' in dirs:
|
||||||
self.media_path_combobox.addItem('/Volumes/' + volume)
|
self.media_path_combobox.addItem('/Volumes/' + volume)
|
||||||
|
# Detect audio cd
|
||||||
|
files = [f for f in dirs if os.path.isfile(f)]
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('aiff'):
|
||||||
|
self.media_path_combobox.addItem('/Volumes/' + volume)
|
||||||
|
break
|
||||||
|
@ -39,7 +39,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi
|
|||||||
build_icon, check_item_selected
|
build_icon, check_item_selected
|
||||||
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
|
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
|
||||||
from openlp.core.ui import DisplayController, Display, DisplayControllerType
|
from openlp.core.ui import DisplayController, Display, DisplayControllerType
|
||||||
from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path
|
from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds
|
||||||
from openlp.core.utils import get_locale_key
|
from openlp.core.utils import get_locale_key
|
||||||
from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE
|
from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE
|
||||||
if VLC_AVAILABLE:
|
if VLC_AVAILABLE:
|
||||||
@ -125,9 +125,20 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
"""
|
"""
|
||||||
Adds buttons to the start of the header bar.
|
Adds buttons to the start of the header bar.
|
||||||
"""
|
"""
|
||||||
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text='Load CD/DVD',
|
print(get_media_players()[0])
|
||||||
tooltip='Load CD/DVD', triggers=self.on_load_optical)
|
if 'vlc' in get_media_players()[0]:
|
||||||
if not VLC_AVAILABLE:
|
diable_optical_button_text = False
|
||||||
|
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||||
|
optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||||
|
else:
|
||||||
|
diable_optical_button_text = True
|
||||||
|
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||||
|
optical_button_tooltip = translate('MediaPlugin.MediaItem',
|
||||||
|
'Load CD/DVD - only supported when VLC is installed and enabled')
|
||||||
|
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text=optical_button_text,
|
||||||
|
tooltip=optical_button_tooltip,
|
||||||
|
triggers=self.on_load_optical)
|
||||||
|
if diable_optical_button_text:
|
||||||
self.load_optical.setDisabled(True)
|
self.load_optical.setDisabled(True)
|
||||||
|
|
||||||
def add_end_header_bar(self):
|
def add_end_header_bar(self):
|
||||||
@ -224,7 +235,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
filename = item.data(QtCore.Qt.UserRole)
|
filename = item.data(QtCore.Qt.UserRole)
|
||||||
# Special handling if the filename is a optical clip
|
# Special handling if the filename is a optical clip
|
||||||
if filename.startswith('optical:'):
|
if filename.startswith('optical:'):
|
||||||
(name, title, audio_track, subtitle_track, start, end) = parse_optical_path(filename)
|
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
|
||||||
if not os.path.exists(name):
|
if not os.path.exists(name):
|
||||||
if not remote:
|
if not remote:
|
||||||
# Optical disc is no longer present
|
# Optical disc is no longer present
|
||||||
@ -234,7 +245,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
return False
|
return False
|
||||||
service_item.processor = self.display_type_combo_box.currentText()
|
service_item.processor = self.display_type_combo_box.currentText()
|
||||||
service_item.add_from_command(filename, name, CLAPPERBOARD)
|
service_item.add_from_command(filename, name, CLAPPERBOARD)
|
||||||
service_item.title = name + '@' + self.format_milliseconds(start) + '-' + self.format_milliseconds(end)
|
service_item.title = clip_name
|
||||||
# Only set start and end times if going to a service
|
# Only set start and end times if going to a service
|
||||||
#if context == ServiceItemContext.Service:
|
#if context == ServiceItemContext.Service:
|
||||||
# Set the length
|
# Set the length
|
||||||
@ -345,12 +356,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
track_info = QtCore.QFileInfo(track)
|
track_info = QtCore.QFileInfo(track)
|
||||||
if track.startswith('optical:'):
|
if track.startswith('optical:'):
|
||||||
# Handle optical based item
|
# Handle optical based item
|
||||||
(file_name, title, audio_track, subtitle_track, start, end) = parse_optical_path(track)
|
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
|
||||||
optical = file_name + '@' + self.format_milliseconds(start) + '-' + self.format_milliseconds(end)
|
item_name = QtGui.QListWidgetItem(clip_name)
|
||||||
item_name = QtGui.QListWidgetItem(optical)
|
|
||||||
item_name.setIcon(OPTICAL_ICON)
|
item_name.setIcon(OPTICAL_ICON)
|
||||||
item_name.setData(QtCore.Qt.UserRole, track)
|
item_name.setData(QtCore.Qt.UserRole, track)
|
||||||
item_name.setToolTip(optical)
|
item_name.setToolTip(file_name + '@' + format_milliseconds(start) + '-' + format_milliseconds(end))
|
||||||
elif not os.path.exists(track):
|
elif not os.path.exists(track):
|
||||||
# File doesn't exist, mark as error.
|
# File doesn't exist, mark as error.
|
||||||
file_name = os.path.split(str(track))[1]
|
file_name = os.path.split(str(track))[1]
|
||||||
@ -417,17 +427,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
:param optical: The clip to add.
|
:param optical: The clip to add.
|
||||||
"""
|
"""
|
||||||
full_list = self.get_file_list()
|
full_list = self.get_file_list()
|
||||||
|
# If the clip already is in the media list it isn't added and an error message is displayed.
|
||||||
|
if optical in full_list:
|
||||||
|
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'),
|
||||||
|
translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved'))
|
||||||
|
return
|
||||||
|
# Append the optical string to the media list
|
||||||
full_list.append(optical)
|
full_list.append(optical)
|
||||||
self.load_list([optical])
|
self.load_list([optical])
|
||||||
Settings().setValue(self.settings_section + '/media files', self.get_file_list())
|
Settings().setValue(self.settings_section + '/media files', self.get_file_list())
|
||||||
|
|
||||||
def format_milliseconds(self, milliseconds):
|
|
||||||
"""
|
|
||||||
Format milliseconds into a human readable time string.
|
|
||||||
:param milliseconds: Milliseconds to format
|
|
||||||
:return: Time string in format: hh.mm.ss,ttt
|
|
||||||
"""
|
|
||||||
seconds, millis = divmod(milliseconds, 1000)
|
|
||||||
minutes, seconds = divmod(seconds, 60)
|
|
||||||
hours, minutes = divmod(minutes, 60)
|
|
||||||
return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user