Merge branch 'transcoding_progress'
This probably broke stuff
This commit is contained in:
commit
23af1d8cbc
1
.gitignore
vendored
1
.gitignore
vendored
@ -64,3 +64,4 @@ venv*
|
|||||||
/extlib/leaflet/
|
/extlib/leaflet/
|
||||||
/extlib/tinymce/
|
/extlib/tinymce/
|
||||||
/extlib/video.js/
|
/extlib/video.js/
|
||||||
|
/extlib/videojs-resolution-switcher
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jquery": "~2.1.3",
|
"jquery": "~2.1.3",
|
||||||
"video.js": "~4.11.4",
|
"video.js": "~5.20.1",
|
||||||
|
"videojs-resolution-switcher": "~0.4.2",
|
||||||
"leaflet": "~0.7.3"
|
"leaflet": "~0.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,7 @@ CELERY_RESULT_DBURI = string(default="sqlite:///%(here)s/celery.db")
|
|||||||
|
|
||||||
# default kombu stuff
|
# default kombu stuff
|
||||||
BROKER_URL = string(default="amqp://")
|
BROKER_URL = string(default="amqp://")
|
||||||
|
CELERY_DEFAULT_QUEUE = string(default="default")
|
||||||
|
|
||||||
# known booleans
|
# known booleans
|
||||||
CELERY_RESULT_PERSISTENT = boolean()
|
CELERY_RESULT_PERSISTENT = boolean()
|
||||||
@ -165,7 +166,7 @@ CELERY_EAGER_PROPAGATES_EXCEPTIONS = boolean()
|
|||||||
CELERY_IGNORE_RESULT = boolean()
|
CELERY_IGNORE_RESULT = boolean()
|
||||||
CELERY_TRACK_STARTED = boolean()
|
CELERY_TRACK_STARTED = boolean()
|
||||||
CELERY_DISABLE_RATE_LIMITS = boolean()
|
CELERY_DISABLE_RATE_LIMITS = boolean()
|
||||||
CELERY_ACKS_LATE = boolean()
|
CELERY_ACKS_LATE = boolean(default=True)
|
||||||
CELERY_STORE_ERRORS_EVEN_IF_IGNORED = boolean()
|
CELERY_STORE_ERRORS_EVEN_IF_IGNORED = boolean()
|
||||||
CELERY_SEND_TASK_ERROR_EMAILS = boolean()
|
CELERY_SEND_TASK_ERROR_EMAILS = boolean()
|
||||||
CELERY_SEND_EVENTS = boolean()
|
CELERY_SEND_EVENTS = boolean()
|
||||||
@ -174,8 +175,8 @@ CELERYD_LOG_COLOR = boolean()
|
|||||||
CELERY_REDIRECT_STDOUTS = boolean()
|
CELERY_REDIRECT_STDOUTS = boolean()
|
||||||
|
|
||||||
# known ints
|
# known ints
|
||||||
CELERYD_CONCURRENCY = integer()
|
CELERYD_CONCURRENCY = integer(default=1)
|
||||||
CELERYD_PREFETCH_MULTIPLIER = integer()
|
CELERYD_PREFETCH_MULTIPLIER = integer(default=1)
|
||||||
CELERY_AMQP_TASK_RESULT_EXPIRES = integer()
|
CELERY_AMQP_TASK_RESULT_EXPIRES = integer()
|
||||||
CELERY_AMQP_TASK_RESULT_CONNECTION_MAX = integer()
|
CELERY_AMQP_TASK_RESULT_CONNECTION_MAX = integer()
|
||||||
REDIS_PORT = integer()
|
REDIS_PORT = integer()
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
"""add main transcoding progress column to MediaEntry
|
||||||
|
|
||||||
|
Revision ID: cc3651803714
|
||||||
|
Revises: 228916769bd2
|
||||||
|
Create Date: 2017-08-21 23:33:01.401589
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'cc3651803714'
|
||||||
|
down_revision = '228916769bd2'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
"""
|
||||||
|
Addition of main_transcoding_progress is required to save the progress of the
|
||||||
|
default resolution (other than the total progress of the video).
|
||||||
|
"""
|
||||||
|
op.add_column('core__media_entries', sa.Column('main_transcoding_progress', sa.Float(), default=0))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
@ -249,6 +249,33 @@ class MediaEntryMixin(GenerateSlugMixin, GeneratePublicIDMixin):
|
|||||||
if media_size in media_sizes:
|
if media_size in media_sizes:
|
||||||
return media_size, self.media_files[media_size]
|
return media_size, self.media_files[media_size]
|
||||||
|
|
||||||
|
def get_all_media(self):
|
||||||
|
"""
|
||||||
|
Returns all available qualties of a media
|
||||||
|
"""
|
||||||
|
fetch_order = self.media_manager.media_fetch_order
|
||||||
|
|
||||||
|
# No fetching order found? well, give up!
|
||||||
|
if not fetch_order:
|
||||||
|
return None
|
||||||
|
|
||||||
|
media_sizes = self.media_files.keys()
|
||||||
|
|
||||||
|
all_media_path = []
|
||||||
|
|
||||||
|
for media_size in fetch_order:
|
||||||
|
if media_size in media_sizes:
|
||||||
|
file_metadata = self.get_file_metadata(media_size)
|
||||||
|
size = file_metadata['medium_size']
|
||||||
|
if media_size != 'webm_video':
|
||||||
|
all_media_path.append((media_size[5:], size,
|
||||||
|
self.media_files[media_size]))
|
||||||
|
else:
|
||||||
|
all_media_path.append(('default', size,
|
||||||
|
self.media_files[media_size]))
|
||||||
|
|
||||||
|
return all_media_path
|
||||||
|
|
||||||
def main_mediafile(self):
|
def main_mediafile(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import datetime
|
|||||||
|
|
||||||
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
||||||
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
||||||
SmallInteger, Date, types
|
SmallInteger, Date, types, Float
|
||||||
from sqlalchemy.orm import relationship, backref, with_polymorphic, validates, \
|
from sqlalchemy.orm import relationship, backref, with_polymorphic, validates, \
|
||||||
class_mapper
|
class_mapper
|
||||||
from sqlalchemy.orm.collections import attribute_mapped_collection
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||||
@ -543,7 +543,8 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
|
|||||||
fail_error = Column(Unicode)
|
fail_error = Column(Unicode)
|
||||||
fail_metadata = Column(JSONEncoded)
|
fail_metadata = Column(JSONEncoded)
|
||||||
|
|
||||||
transcoding_progress = Column(SmallInteger)
|
transcoding_progress = Column(Float, default=0)
|
||||||
|
main_transcoding_progress = Column(Float, default=0)
|
||||||
|
|
||||||
queued_media_file = Column(PathTupleWithSlashes)
|
queued_media_file = Column(PathTupleWithSlashes)
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import logging
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
from kombu import Exchange, Queue
|
||||||
from mediagoblin.tools.pluginapi import hook_runall
|
from mediagoblin.tools.pluginapi import hook_runall
|
||||||
|
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ MANDATORY_CELERY_IMPORTS = [
|
|||||||
'mediagoblin.processing.task',
|
'mediagoblin.processing.task',
|
||||||
'mediagoblin.notifications.task',
|
'mediagoblin.notifications.task',
|
||||||
'mediagoblin.submit.task',
|
'mediagoblin.submit.task',
|
||||||
|
'mediagoblin.media_types.video.processing',
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
|
DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
|
||||||
@ -47,6 +49,12 @@ def get_celery_settings_dict(app_config, global_config,
|
|||||||
else:
|
else:
|
||||||
celery_conf = {}
|
celery_conf = {}
|
||||||
|
|
||||||
|
# Add x-max-priority to config
|
||||||
|
celery_conf['CELERY_QUEUES'] = (
|
||||||
|
Queue('default', Exchange('default'), routing_key='default',
|
||||||
|
queue_arguments={'x-max-priority': 10}),
|
||||||
|
)
|
||||||
|
|
||||||
celery_settings = {}
|
celery_settings = {}
|
||||||
|
|
||||||
# Add all celery settings from config
|
# Add all celery settings from config
|
||||||
|
@ -431,6 +431,7 @@ class ImageProcessingManager(ProcessingManager):
|
|||||||
self.add_processor(Resizer)
|
self.add_processor(Resizer)
|
||||||
self.add_processor(MetadataProcessing)
|
self.add_processor(MetadataProcessing)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
import pprint
|
import pprint
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from mediagoblin import mg_globals as mgg
|
||||||
from mediagoblin.media_types import MediaManagerBase
|
from mediagoblin.media_types import MediaManagerBase
|
||||||
from mediagoblin.media_types.video.processing import (VideoProcessingManager,
|
from mediagoblin.media_types.video.processing import (VideoProcessingManager,
|
||||||
sniff_handler, sniffer)
|
sniff_handler, sniffer)
|
||||||
@ -31,9 +32,17 @@ class VideoMediaManager(MediaManagerBase):
|
|||||||
type_icon = "images/type_icons/video.png"
|
type_icon = "images/type_icons/video.png"
|
||||||
|
|
||||||
# Used by the media_entry.get_display_media method
|
# Used by the media_entry.get_display_media method
|
||||||
media_fetch_order = [u'webm_video', u'original']
|
|
||||||
default_webm_type = 'video/webm; codecs="vp8, vorbis"'
|
default_webm_type = 'video/webm; codecs="vp8, vorbis"'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_fetch_order(self):
|
||||||
|
video_config = mgg.global_config['plugins'][MEDIA_TYPE]
|
||||||
|
video_res = video_config['available_resolutions']
|
||||||
|
video_res.remove(video_config['default_resolution'])
|
||||||
|
video_res.insert(0, video_config['default_resolution'])
|
||||||
|
video_res = ['webm_{}'.format(x) for x in video_res]
|
||||||
|
return ([u'webm_video'] + video_res + [u'original'])
|
||||||
|
|
||||||
|
|
||||||
def get_media_type_and_manager(ext):
|
def get_media_type_and_manager(ext):
|
||||||
if ext in ACCEPTED_EXTENSIONS:
|
if ext in ACCEPTED_EXTENSIONS:
|
||||||
|
@ -12,6 +12,14 @@ vorbis_quality = float(default=0.3)
|
|||||||
# Autoplay the video when page is loaded?
|
# Autoplay the video when page is loaded?
|
||||||
auto_play = boolean(default=False)
|
auto_play = boolean(default=False)
|
||||||
|
|
||||||
|
# List of resolutions that the video should be transcoded to
|
||||||
|
# Choose among ['144p', '240p', '360p', '480p', '720p', '1080p'],
|
||||||
|
# preferrably in the order of transcoding.
|
||||||
|
available_resolutions = string_list(default=list('480p', '360p', '720p'))
|
||||||
|
|
||||||
|
# Default resolution of video
|
||||||
|
default_resolution = string(default='480p')
|
||||||
|
|
||||||
[[skip_transcode]]
|
[[skip_transcode]]
|
||||||
mime_types = string_list(default=list("video/webm"))
|
mime_types = string_list(default=list("video/webm"))
|
||||||
container_formats = string_list(default=list("Matroska"))
|
container_formats = string_list(default=list("Matroska"))
|
||||||
|
@ -18,21 +18,23 @@ import argparse
|
|||||||
import os.path
|
import os.path
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
|
import celery
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from celery import group
|
||||||
from mediagoblin import mg_globals as mgg
|
from mediagoblin import mg_globals as mgg
|
||||||
from mediagoblin.processing import (
|
from mediagoblin.processing import (
|
||||||
FilenameBuilder, BaseProcessingFail,
|
FilenameBuilder, BaseProcessingFail,
|
||||||
ProgressCallback, MediaProcessor,
|
ProgressCallback, MediaProcessor,
|
||||||
ProcessingManager, request_from_args,
|
ProcessingManager, request_from_args,
|
||||||
get_process_filename, store_public,
|
get_process_filename, store_public,
|
||||||
copy_original)
|
copy_original, get_entry_and_processing_manager)
|
||||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||||
from mediagoblin.media_types import MissingComponents
|
from mediagoblin.media_types import MissingComponents
|
||||||
|
|
||||||
from . import transcoders
|
from . import transcoders
|
||||||
from .util import skip_transcode
|
from .util import skip_transcode, ACCEPTED_RESOLUTIONS
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
_log.setLevel(logging.DEBUG)
|
_log.setLevel(logging.DEBUG)
|
||||||
@ -173,13 +175,67 @@ def store_metadata(media_entry, metadata):
|
|||||||
media_entry.media_data_init(orig_metadata=stored_metadata)
|
media_entry.media_data_init(orig_metadata=stored_metadata)
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def main_task(entry_id, resolution, medium_size, **process_info):
|
||||||
|
"""
|
||||||
|
Main celery task to transcode the video to the default resolution
|
||||||
|
and store original video metadata.
|
||||||
|
"""
|
||||||
|
_log.debug('MediaEntry processing')
|
||||||
|
entry, manager = get_entry_and_processing_manager(entry_id)
|
||||||
|
with CommonVideoProcessor(manager, entry) as processor:
|
||||||
|
processor.common_setup(resolution)
|
||||||
|
processor.transcode(medium_size=tuple(medium_size),
|
||||||
|
vp8_quality=process_info['vp8_quality'],
|
||||||
|
vp8_threads=process_info['vp8_threads'],
|
||||||
|
vorbis_quality=process_info['vorbis_quality'])
|
||||||
|
processor.generate_thumb(thumb_size=process_info['thumb_size'])
|
||||||
|
processor.store_orig_metadata()
|
||||||
|
# Make state of entry as processed
|
||||||
|
entry.state = u'processed'
|
||||||
|
entry.save()
|
||||||
|
_log.info(u'MediaEntry ID {0} is processed (transcoded to default'
|
||||||
|
' resolution): {1}'.format(entry.id, medium_size))
|
||||||
|
_log.debug('MediaEntry processed')
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def complementary_task(entry_id, resolution, medium_size, **process_info):
|
||||||
|
"""
|
||||||
|
Side celery task to transcode the video to other resolutions
|
||||||
|
"""
|
||||||
|
entry, manager = get_entry_and_processing_manager(entry_id)
|
||||||
|
with CommonVideoProcessor(manager, entry) as processor:
|
||||||
|
processor.common_setup(resolution)
|
||||||
|
processor.transcode(medium_size=tuple(medium_size),
|
||||||
|
vp8_quality=process_info['vp8_quality'],
|
||||||
|
vp8_threads=process_info['vp8_threads'],
|
||||||
|
vorbis_quality=process_info['vorbis_quality'])
|
||||||
|
_log.info(u'MediaEntry ID {0} is transcoded to {1}'.format(
|
||||||
|
entry.id, medium_size))
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def processing_cleanup(entry_id):
|
||||||
|
_log.debug('Entered processing_cleanup')
|
||||||
|
entry, manager = get_entry_and_processing_manager(entry_id)
|
||||||
|
with CommonVideoProcessor(manager, entry) as processor:
|
||||||
|
# no need to specify a resolution here
|
||||||
|
processor.common_setup()
|
||||||
|
processor.copy_original()
|
||||||
|
processor.keep_best()
|
||||||
|
processor.delete_queue_file()
|
||||||
|
_log.debug('Deleted queue_file')
|
||||||
|
|
||||||
|
|
||||||
class CommonVideoProcessor(MediaProcessor):
|
class CommonVideoProcessor(MediaProcessor):
|
||||||
"""
|
"""
|
||||||
Provides a base for various video processing steps
|
Provides a base for various video processing steps
|
||||||
"""
|
"""
|
||||||
acceptable_files = ['original', 'best_quality', 'webm_video']
|
acceptable_files = ['original, best_quality', 'webm_144p', 'webm_360p',
|
||||||
|
'webm_480p', 'webm_720p', 'webm_1080p', 'webm_video']
|
||||||
|
|
||||||
def common_setup(self):
|
def common_setup(self, resolution=None):
|
||||||
self.video_config = mgg \
|
self.video_config = mgg \
|
||||||
.global_config['plugins'][MEDIA_TYPE]
|
.global_config['plugins'][MEDIA_TYPE]
|
||||||
|
|
||||||
@ -191,24 +247,47 @@ class CommonVideoProcessor(MediaProcessor):
|
|||||||
self.transcoder = transcoders.VideoTranscoder()
|
self.transcoder = transcoders.VideoTranscoder()
|
||||||
self.did_transcode = False
|
self.did_transcode = False
|
||||||
|
|
||||||
|
if resolution:
|
||||||
|
self.curr_file = 'webm_' + str(resolution)
|
||||||
|
self.part_filename = (self.name_builder.fill('{basename}.' +
|
||||||
|
str(resolution) + '.webm'))
|
||||||
|
else:
|
||||||
|
self.curr_file = 'webm_video'
|
||||||
|
self.part_filename = self.name_builder.fill('{basename}.medium.webm')
|
||||||
|
|
||||||
|
|
||||||
def copy_original(self):
|
def copy_original(self):
|
||||||
# If we didn't transcode, then we need to keep the original
|
# If we didn't transcode, then we need to keep the original
|
||||||
|
self.did_transcode = False
|
||||||
|
for each_res in self.video_config['available_resolutions']:
|
||||||
|
if ('webm_' + str(each_res)) in self.entry.media_files:
|
||||||
|
self.did_transcode = True
|
||||||
|
break
|
||||||
if not self.did_transcode or \
|
if not self.did_transcode or \
|
||||||
(self.video_config['keep_original'] and self.did_transcode):
|
(self.video_config['keep_original'] and self.did_transcode):
|
||||||
copy_original(
|
copy_original(
|
||||||
self.entry, self.process_filename,
|
self.entry, self.process_filename,
|
||||||
self.name_builder.fill('{basename}{ext}'))
|
self.name_builder.fill('{basename}{ext}'))
|
||||||
|
|
||||||
def _keep_best(self):
|
|
||||||
|
def keep_best(self):
|
||||||
"""
|
"""
|
||||||
If there is no original, keep the best file that we have
|
If there is no original, keep the best file that we have
|
||||||
"""
|
"""
|
||||||
|
best_file = None
|
||||||
|
best_file_dim = (0, 0)
|
||||||
|
for each_res in self.video_config['available_resolutions']:
|
||||||
|
curr_dim = ACCEPTED_RESOLUTIONS[each_res]
|
||||||
|
if curr_dim[0] >= best_file_dim[0] and curr_dim[1] >= best_file_dim[1]:
|
||||||
|
best_file = each_res
|
||||||
|
best_file_dim = curr_dim
|
||||||
if not self.entry.media_files.get('best_quality'):
|
if not self.entry.media_files.get('best_quality'):
|
||||||
# Save the best quality file if no original?
|
# Save the best quality file if no original?
|
||||||
if not self.entry.media_files.get('original') and \
|
if not self.entry.media_files.get('original') and \
|
||||||
self.entry.media_files.get('webm_video'):
|
self.entry.media_files.get(str(best_file)):
|
||||||
self.entry.media_files['best_quality'] = self.entry \
|
self.entry.media_files['best_quality'] = self.entry \
|
||||||
.media_files['webm_video']
|
.media_files[str(best_file)]
|
||||||
|
|
||||||
|
|
||||||
def _skip_processing(self, keyname, **kwargs):
|
def _skip_processing(self, keyname, **kwargs):
|
||||||
file_metadata = self.entry.get_file_metadata(keyname)
|
file_metadata = self.entry.get_file_metadata(keyname)
|
||||||
@ -217,7 +296,7 @@ class CommonVideoProcessor(MediaProcessor):
|
|||||||
return False
|
return False
|
||||||
skip = True
|
skip = True
|
||||||
|
|
||||||
if keyname == 'webm_video':
|
if 'webm' in keyname:
|
||||||
if kwargs.get('medium_size') != file_metadata.get('medium_size'):
|
if kwargs.get('medium_size') != file_metadata.get('medium_size'):
|
||||||
skip = False
|
skip = False
|
||||||
elif kwargs.get('vp8_quality') != file_metadata.get('vp8_quality'):
|
elif kwargs.get('vp8_quality') != file_metadata.get('vp8_quality'):
|
||||||
@ -237,8 +316,7 @@ class CommonVideoProcessor(MediaProcessor):
|
|||||||
def transcode(self, medium_size=None, vp8_quality=None, vp8_threads=None,
|
def transcode(self, medium_size=None, vp8_quality=None, vp8_threads=None,
|
||||||
vorbis_quality=None):
|
vorbis_quality=None):
|
||||||
progress_callback = ProgressCallback(self.entry)
|
progress_callback = ProgressCallback(self.entry)
|
||||||
tmp_dst = os.path.join(self.workbench.dir,
|
tmp_dst = os.path.join(self.workbench.dir, self.part_filename)
|
||||||
self.name_builder.fill('{basename}.medium.webm'))
|
|
||||||
|
|
||||||
if not medium_size:
|
if not medium_size:
|
||||||
medium_size = (
|
medium_size = (
|
||||||
@ -256,17 +334,10 @@ class CommonVideoProcessor(MediaProcessor):
|
|||||||
'vp8_quality': vp8_quality,
|
'vp8_quality': vp8_quality,
|
||||||
'vorbis_quality': vorbis_quality}
|
'vorbis_quality': vorbis_quality}
|
||||||
|
|
||||||
if self._skip_processing('webm_video', **file_metadata):
|
if self._skip_processing(self.curr_file, **file_metadata):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Extract metadata and keep a record of it
|
|
||||||
metadata = transcoders.discover(self.process_filename)
|
metadata = transcoders.discover(self.process_filename)
|
||||||
|
|
||||||
# metadata's stream info here is a DiscovererContainerInfo instance,
|
|
||||||
# it gets split into DiscovererAudioInfo and DiscovererVideoInfo;
|
|
||||||
# metadata itself has container-related data in tags, like video-codec
|
|
||||||
store_metadata(self.entry, metadata)
|
|
||||||
|
|
||||||
orig_dst_dimensions = (metadata.get_video_streams()[0].get_width(),
|
orig_dst_dimensions = (metadata.get_video_streams()[0].get_width(),
|
||||||
metadata.get_video_streams()[0].get_height())
|
metadata.get_video_streams()[0].get_height())
|
||||||
|
|
||||||
@ -275,45 +346,37 @@ class CommonVideoProcessor(MediaProcessor):
|
|||||||
if skip_transcode(metadata, medium_size):
|
if skip_transcode(metadata, medium_size):
|
||||||
_log.debug('Skipping transcoding')
|
_log.debug('Skipping transcoding')
|
||||||
|
|
||||||
dst_dimensions = orig_dst_dimensions
|
|
||||||
|
|
||||||
# If there is an original and transcoded, delete the transcoded
|
# If there is an original and transcoded, delete the transcoded
|
||||||
# since it must be of lower quality then the original
|
# since it must be of lower quality then the original
|
||||||
if self.entry.media_files.get('original') and \
|
if self.entry.media_files.get('original') and \
|
||||||
self.entry.media_files.get('webm_video'):
|
self.entry.media_files.get(self.curr_file):
|
||||||
self.entry.media_files['webm_video'].delete()
|
self.entry.media_files[self.curr_file].delete()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
_log.debug('Entered transcoder')
|
||||||
|
video_config = (mgg.global_config['plugins']
|
||||||
|
['mediagoblin.media_types.video'])
|
||||||
|
num_res = len(video_config['available_resolutions'])
|
||||||
|
default_res = video_config['default_resolution']
|
||||||
self.transcoder.transcode(self.process_filename, tmp_dst,
|
self.transcoder.transcode(self.process_filename, tmp_dst,
|
||||||
|
default_res, num_res,
|
||||||
vp8_quality=vp8_quality,
|
vp8_quality=vp8_quality,
|
||||||
vp8_threads=vp8_threads,
|
vp8_threads=vp8_threads,
|
||||||
vorbis_quality=vorbis_quality,
|
vorbis_quality=vorbis_quality,
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
dimensions=tuple(medium_size))
|
dimensions=tuple(medium_size))
|
||||||
if self.transcoder.dst_data:
|
if self.transcoder.dst_data:
|
||||||
video_info = self.transcoder.dst_data.get_video_streams()[0]
|
|
||||||
dst_dimensions = (video_info.get_width(),
|
|
||||||
video_info.get_height())
|
|
||||||
self._keep_best()
|
|
||||||
|
|
||||||
# Push transcoded video to public storage
|
# Push transcoded video to public storage
|
||||||
_log.debug('Saving medium...')
|
_log.debug('Saving medium...')
|
||||||
store_public(self.entry, 'webm_video', tmp_dst,
|
store_public(self.entry, self.curr_file, tmp_dst, self.part_filename)
|
||||||
self.name_builder.fill('{basename}.medium.webm'))
|
|
||||||
_log.debug('Saved medium')
|
_log.debug('Saved medium')
|
||||||
|
|
||||||
self.entry.set_file_metadata('webm_video', **file_metadata)
|
self.entry.set_file_metadata(self.curr_file, **file_metadata)
|
||||||
|
|
||||||
self.did_transcode = True
|
self.did_transcode = True
|
||||||
else:
|
|
||||||
dst_dimensions = orig_dst_dimensions
|
|
||||||
|
|
||||||
# Save the width and height of the transcoded video
|
|
||||||
self.entry.media_data_init(
|
|
||||||
width=dst_dimensions[0],
|
|
||||||
height=dst_dimensions[1])
|
|
||||||
|
|
||||||
def generate_thumb(self, thumb_size=None):
|
def generate_thumb(self, thumb_size=None):
|
||||||
|
_log.debug("Enter generate_thumb()")
|
||||||
# Temporary file for the video thumbnail (cleaned up with workbench)
|
# Temporary file for the video thumbnail (cleaned up with workbench)
|
||||||
tmp_thumb = os.path.join(self.workbench.dir,
|
tmp_thumb = os.path.join(self.workbench.dir,
|
||||||
self.name_builder.fill(
|
self.name_builder.fill(
|
||||||
@ -343,6 +406,17 @@ class CommonVideoProcessor(MediaProcessor):
|
|||||||
|
|
||||||
self.entry.set_file_metadata('thumb', thumb_size=thumb_size)
|
self.entry.set_file_metadata('thumb', thumb_size=thumb_size)
|
||||||
|
|
||||||
|
def store_orig_metadata(self):
|
||||||
|
# Extract metadata and keep a record of it
|
||||||
|
metadata = transcoders.discover(self.process_filename)
|
||||||
|
|
||||||
|
# metadata's stream info here is a DiscovererContainerInfo instance,
|
||||||
|
# it gets split into DiscovererAudioInfo and DiscovererVideoInfo;
|
||||||
|
# metadata itself has container-related data in tags, like video-codec
|
||||||
|
store_metadata(self.entry, metadata)
|
||||||
|
_log.debug("Stored original video metadata")
|
||||||
|
|
||||||
|
|
||||||
class InitialProcessor(CommonVideoProcessor):
|
class InitialProcessor(CommonVideoProcessor):
|
||||||
"""
|
"""
|
||||||
Initial processing steps for new video
|
Initial processing steps for new video
|
||||||
@ -399,13 +473,12 @@ class InitialProcessor(CommonVideoProcessor):
|
|||||||
'vorbis_quality', 'thumb_size'])
|
'vorbis_quality', 'thumb_size'])
|
||||||
|
|
||||||
def process(self, medium_size=None, vp8_threads=None, vp8_quality=None,
|
def process(self, medium_size=None, vp8_threads=None, vp8_quality=None,
|
||||||
vorbis_quality=None, thumb_size=None):
|
vorbis_quality=None, thumb_size=None, resolution=None):
|
||||||
self.common_setup()
|
self.common_setup(resolution=resolution)
|
||||||
|
self.store_orig_metadata()
|
||||||
self.transcode(medium_size=medium_size, vp8_quality=vp8_quality,
|
self.transcode(medium_size=medium_size, vp8_quality=vp8_quality,
|
||||||
vp8_threads=vp8_threads, vorbis_quality=vorbis_quality)
|
vp8_threads=vp8_threads, vorbis_quality=vorbis_quality)
|
||||||
|
|
||||||
self.copy_original()
|
|
||||||
self.generate_thumb(thumb_size=thumb_size)
|
self.generate_thumb(thumb_size=thumb_size)
|
||||||
self.delete_queue_file()
|
self.delete_queue_file()
|
||||||
|
|
||||||
@ -516,3 +589,43 @@ class VideoProcessingManager(ProcessingManager):
|
|||||||
self.add_processor(InitialProcessor)
|
self.add_processor(InitialProcessor)
|
||||||
self.add_processor(Resizer)
|
self.add_processor(Resizer)
|
||||||
self.add_processor(Transcoder)
|
self.add_processor(Transcoder)
|
||||||
|
|
||||||
|
def workflow(self, entry, feed_url, reprocess_action, reprocess_info=None):
|
||||||
|
|
||||||
|
video_config = mgg.global_config['plugins'][MEDIA_TYPE]
|
||||||
|
def_res = video_config['default_resolution']
|
||||||
|
priority_num = len(video_config['available_resolutions']) + 1
|
||||||
|
|
||||||
|
entry.state = u'processing'
|
||||||
|
entry.save()
|
||||||
|
|
||||||
|
reprocess_info = reprocess_info or {}
|
||||||
|
if 'vp8_quality' not in reprocess_info:
|
||||||
|
reprocess_info['vp8_quality'] = None
|
||||||
|
if 'vorbis_quality' not in reprocess_info:
|
||||||
|
reprocess_info['vorbis_quality'] = None
|
||||||
|
if 'vp8_threads' not in reprocess_info:
|
||||||
|
reprocess_info['vp8_threads'] = None
|
||||||
|
if 'thumb_size' not in reprocess_info:
|
||||||
|
reprocess_info['thumb_size'] = None
|
||||||
|
|
||||||
|
tasks_list = [main_task.signature(args=(entry.id, def_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[def_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)]
|
||||||
|
|
||||||
|
for comp_res in video_config['available_resolutions']:
|
||||||
|
if comp_res != def_res:
|
||||||
|
priority_num += -1
|
||||||
|
tasks_list.append(
|
||||||
|
complementary_task.signature(args=(entry.id, comp_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[comp_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
transcoding_tasks = group(tasks_list)
|
||||||
|
cleanup_task = processing_cleanup.signature(args=(entry.id,),
|
||||||
|
queue='default', immutable=True)
|
||||||
|
|
||||||
|
return (transcoding_tasks, cleanup_task)
|
||||||
|
@ -21,8 +21,10 @@ import sys
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
|
from mediagoblin import mg_globals as mgg
|
||||||
from mediagoblin.media_types.tools import discover
|
from mediagoblin.media_types.tools import discover
|
||||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||||
|
from .util import ACCEPTED_RESOLUTIONS
|
||||||
|
|
||||||
#os.environ['GST_DEBUG'] = '4,python:4'
|
#os.environ['GST_DEBUG'] = '4,python:4'
|
||||||
|
|
||||||
@ -153,10 +155,10 @@ class VideoTranscoder(object):
|
|||||||
'''
|
'''
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
_log.info('Initializing VideoTranscoder...')
|
_log.info('Initializing VideoTranscoder...')
|
||||||
self.progress_percentage = None
|
self.progress_percentage = 0
|
||||||
self.loop = GLib.MainLoop()
|
self.loop = GLib.MainLoop()
|
||||||
|
|
||||||
def transcode(self, src, dst, **kwargs):
|
def transcode(self, src, dst, default_res, num_res, **kwargs):
|
||||||
'''
|
'''
|
||||||
Transcode a video file into a 'medium'-sized version.
|
Transcode a video file into a 'medium'-sized version.
|
||||||
'''
|
'''
|
||||||
@ -184,6 +186,10 @@ class VideoTranscoder(object):
|
|||||||
|
|
||||||
self._progress_callback = kwargs.get('progress_callback') or None
|
self._progress_callback = kwargs.get('progress_callback') or None
|
||||||
|
|
||||||
|
# Get number of resolutions available for the video
|
||||||
|
self.num_of_resolutions = num_res
|
||||||
|
self.default_resolution = default_res
|
||||||
|
|
||||||
if not type(self.destination_dimensions) == tuple:
|
if not type(self.destination_dimensions) == tuple:
|
||||||
raise Exception('dimensions must be tuple: (width, height)')
|
raise Exception('dimensions must be tuple: (width, height)')
|
||||||
|
|
||||||
@ -354,10 +360,19 @@ class VideoTranscoder(object):
|
|||||||
# Update progress state if it has changed
|
# Update progress state if it has changed
|
||||||
(success, percent) = structure.get_int('percent')
|
(success, percent) = structure.get_int('percent')
|
||||||
if self.progress_percentage != percent and success:
|
if self.progress_percentage != percent and success:
|
||||||
|
# FIXME: the code below is a workaround for structure.get_int('percent')
|
||||||
|
# returning 0 when the transcoding gets over (100%)
|
||||||
|
if self.progress_percentage > percent and percent == 0:
|
||||||
|
percent = 100
|
||||||
|
percent_increment = percent - self.progress_percentage
|
||||||
self.progress_percentage = percent
|
self.progress_percentage = percent
|
||||||
if self._progress_callback:
|
if self._progress_callback:
|
||||||
self._progress_callback(percent)
|
if ACCEPTED_RESOLUTIONS[self.default_resolution] == self.destination_dimensions:
|
||||||
_log.info('{percent}% done...'.format(percent=percent))
|
self._progress_callback(percent_increment/self.num_of_resolutions, percent)
|
||||||
|
else:
|
||||||
|
self._progress_callback(percent_increment/self.num_of_resolutions)
|
||||||
|
_log.info('{percent}% of {dest} resolution done..'
|
||||||
|
'.'.format(percent=percent, dest=self.destination_dimensions))
|
||||||
elif message.type == Gst.MessageType.ERROR:
|
elif message.type == Gst.MessageType.ERROR:
|
||||||
_log.error('Got error: {0}'.format(message.parse_error()))
|
_log.error('Got error: {0}'.format(message.parse_error()))
|
||||||
self.dst_data = None
|
self.dst_data = None
|
||||||
|
@ -18,6 +18,16 @@ import logging
|
|||||||
|
|
||||||
from mediagoblin import mg_globals as mgg
|
from mediagoblin import mg_globals as mgg
|
||||||
|
|
||||||
|
ACCEPTED_RESOLUTIONS = {
|
||||||
|
'144p': (256, 144),
|
||||||
|
'240p': (352, 240),
|
||||||
|
'360p': (480, 360),
|
||||||
|
'480p': (858, 480),
|
||||||
|
'720p': (1280, 720),
|
||||||
|
'1080p': (1920, 1080),
|
||||||
|
'webm': (640, 640),
|
||||||
|
}
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,9 +39,14 @@ class ProgressCallback(object):
|
|||||||
def __init__(self, entry):
|
def __init__(self, entry):
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
|
|
||||||
def __call__(self, progress):
|
def __call__(self, progress, default_quality_progress=None):
|
||||||
if progress:
|
if progress:
|
||||||
self.entry.transcoding_progress = progress
|
if 100 - (self.entry.transcoding_progress + progress) < 0.01:
|
||||||
|
self.entry.transcoding_progress = 100
|
||||||
|
else:
|
||||||
|
self.entry.transcoding_progress += round(progress, 2)
|
||||||
|
if default_quality_progress:
|
||||||
|
self.entry.main_transcoding_progress = default_quality_progress
|
||||||
self.entry.save()
|
self.entry.save()
|
||||||
|
|
||||||
|
|
||||||
@ -257,6 +262,12 @@ class ProcessingManager(object):
|
|||||||
|
|
||||||
return processor
|
return processor
|
||||||
|
|
||||||
|
def workflow(self, entry, feed_url, reprocess_action, reprocess_info=None):
|
||||||
|
"""
|
||||||
|
Returns the Celery command needed to proceed with media processing
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def request_from_args(args, which_args):
|
def request_from_args(args, which_args):
|
||||||
"""
|
"""
|
||||||
|
@ -1 +1 @@
|
|||||||
../../../extlib/video.js/dist/video-js
|
../../../extlib/video.js/dist/
|
1
mediagoblin/static/extlib/videojs-resolution-switcher
Symbolic link
1
mediagoblin/static/extlib/videojs-resolution-switcher
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../extlib/videojs-resolution-switcher/lib/
|
27
mediagoblin/static/js/change-video-resolution.js
Normal file
27
mediagoblin/static/js/change-video-resolution.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
var glplayer;
|
||||||
|
|
||||||
|
$(document).ready(function()
|
||||||
|
{
|
||||||
|
// fire up the plugin
|
||||||
|
glplayer = videojs('video_1', {
|
||||||
|
controls: true,
|
||||||
|
muted: true,
|
||||||
|
height: 400,
|
||||||
|
width: 700,
|
||||||
|
plugins: {
|
||||||
|
videoJsResolutionSwitcher: {
|
||||||
|
ui: true,
|
||||||
|
default: 'low', // Default resolution [{Number}, 'low', 'high'],
|
||||||
|
dynamicLabel: true // Display dynamic labels or gear symbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, function(){
|
||||||
|
var player = this;
|
||||||
|
window.player = player
|
||||||
|
player.on('resolutionchange', function(){
|
||||||
|
console.info('Source changed to %s', player.src());
|
||||||
|
console.log(player.currentTime());
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
@ -20,6 +20,8 @@ from os.path import splitext
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from celery import chord
|
||||||
|
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from werkzeug.datastructures import FileStorage
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ from mediagoblin.tools.response import json_response
|
|||||||
from mediagoblin.tools.text import convert_to_tag_list_of_dicts
|
from mediagoblin.tools.text import convert_to_tag_list_of_dicts
|
||||||
from mediagoblin.tools.federation import create_activity, create_generator
|
from mediagoblin.tools.federation import create_activity, create_generator
|
||||||
from mediagoblin.db.models import Collection, MediaEntry, ProcessingMetaData
|
from mediagoblin.db.models import Collection, MediaEntry, ProcessingMetaData
|
||||||
from mediagoblin.processing import mark_entry_failed
|
from mediagoblin.processing import mark_entry_failed, get_entry_and_processing_manager
|
||||||
from mediagoblin.processing.task import ProcessMedia
|
from mediagoblin.processing.task import ProcessMedia
|
||||||
from mediagoblin.notifications import add_comment_subscription
|
from mediagoblin.notifications import add_comment_subscription
|
||||||
from mediagoblin.media_types import sniff_media
|
from mediagoblin.media_types import sniff_media
|
||||||
@ -262,10 +264,17 @@ def run_process_media(entry, feed_url=None,
|
|||||||
:param reprocess_action: What particular action should be run.
|
:param reprocess_action: What particular action should be run.
|
||||||
:param reprocess_info: A dict containing all of the necessary reprocessing
|
:param reprocess_info: A dict containing all of the necessary reprocessing
|
||||||
info for the given media_type"""
|
info for the given media_type"""
|
||||||
|
|
||||||
|
entry, manager = get_entry_and_processing_manager(entry.id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
wf = manager.workflow(entry, feed_url, reprocess_action, reprocess_info)
|
||||||
|
if wf is None:
|
||||||
ProcessMedia().apply_async(
|
ProcessMedia().apply_async(
|
||||||
[entry.id, feed_url, reprocess_action, reprocess_info], {},
|
[entry.id, feed_url, reprocess_action, reprocess_info], {},
|
||||||
task_id=entry.queued_task_id)
|
task_id=entry.queued_task_id)
|
||||||
|
else:
|
||||||
|
chord(wf[0])(wf[1])
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
# The purpose of this section is because when running in "lazy"
|
# The purpose of this section is because when running in "lazy"
|
||||||
# or always-eager-with-exceptions-propagated celery mode that
|
# or always-eager-with-exceptions-propagated celery mode that
|
||||||
|
@ -22,15 +22,19 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script type="text/javascript" src="{{
|
<script type="text/javascript" src="{{
|
||||||
request.staticdirect('/extlib/video-js/video.js') }}"></script>
|
request.staticdirect('/extlib/video-js/video.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{
|
||||||
|
request.staticdirect('/extlib/videojs-resolution-switcher/videojs-resolution-switcher.js') }}">
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{{ request.staticdirect('/js/change-video-resolution.js') }}"></script>
|
||||||
{# Sadly commented out till we can get the mediagoblin skin ported over
|
{# Sadly commented out till we can get the mediagoblin skin ported over
|
||||||
# to the newest video.js release ;\ #}
|
# to the newest video.js release ;\ #}
|
||||||
{#
|
|
||||||
<link href="{{ request.staticdirect('/css/vjs-mg-skin.css') }}"
|
|
||||||
rel="stylesheet">
|
|
||||||
#}
|
|
||||||
<link href="{{
|
<link href="{{
|
||||||
request.staticdirect('/extlib/video-js/video-js.css') }}"
|
request.staticdirect('/extlib/video-js/video-js.css') }}"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
|
<link href="{{
|
||||||
|
request.staticdirect('/extlib/videojs-resolution-switcher/videojs-resolution-switcher.css') }}"
|
||||||
|
rel="stylesheet">
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.vjs-default-skin .vjs-big-play-button
|
.vjs-default-skin .vjs-big-play-button
|
||||||
@ -43,27 +47,29 @@
|
|||||||
background-color: #86D4B1 !important;
|
background-color: #86D4B1 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
{% block mediagoblin_media %}
|
{% block mediagoblin_media %}
|
||||||
<div class="media_other_container">
|
<div class="media_other_container">
|
||||||
{% set display_type, display_path = media.get_display_media() %}
|
{% set all_media_path = media.get_all_media() %}
|
||||||
|
|
||||||
<video controls
|
<video controls
|
||||||
{% if global_config['plugins']['mediagoblin.media_types.video']['auto_play'] %}autoplay{% endif %}
|
{% if global_config['plugins']['mediagoblin.media_types.video']['auto_play'] %}autoplay{% endif %}
|
||||||
preload="auto" class="video-js vjs-default-skin"
|
preload="auto" class="video-js vjs-default-skin" id="video_1">
|
||||||
data-setup='{"height": {{ media.media_data.height }},
|
{% for each_media_path in all_media_path %}
|
||||||
"width": {{ media.media_data.width }} }'>
|
<source src="{{ request.app.public_store.file_url(each_media_path[2]) }}"
|
||||||
<source src="{{ request.app.public_store.file_url(display_path) }}"
|
|
||||||
{% if media.media_data %}
|
{% if media.media_data %}
|
||||||
type="{{ media.media_data.source_type() }}"
|
type="{{ media.media_data.source_type() }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
type="{{ media.media_manager['default_webm_type'] }}"
|
type="{{ media.media_manager['default_webm_type'] }}"
|
||||||
{% endif %} />
|
{% endif %}
|
||||||
|
label="{{ each_media_path[0] }}" res="{{ each_media_path[1][1] }}" />
|
||||||
{%- for subtitle in media.subtitle_files %}
|
{%- for subtitle in media.subtitle_files %}
|
||||||
<track src="{{ request.app.public_store.file_url(subtitle.filepath) }}"
|
<track src="{{ request.app.public_store.file_url(subtitle.filepath) }}"
|
||||||
label="{{ subtitle.name }}" kind="subtitles">
|
label="{{ subtitle.name }}" kind="subtitles">
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
{% endfor %}
|
||||||
<div class="no_html5">
|
<div class="no_html5">
|
||||||
{%- trans -%}Sorry, this video will not work because
|
{%- trans -%}Sorry, this video will not work because
|
||||||
your web browser does not support HTML5
|
your web browser does not support HTML5
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
{% trans %}Media processing panel{% endtrans %} — {{ super() }}
|
{% trans %}Media processing panel{% endtrans %} — {{ super() }}
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block mediagoblin_content %}
|
{% block mediagoblin_content %}
|
||||||
|
|
||||||
<h1>{% trans %}Media processing panel{% endtrans %}</h1>
|
<h1>{% trans %}Media processing panel{% endtrans %}</h1>
|
||||||
@ -50,7 +51,8 @@ Show:
|
|||||||
<th width="210">Thumbnail</th>
|
<th width="210">Thumbnail</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th width="20%">When submitted</th>
|
<th width="20%">When submitted</th>
|
||||||
<th width="200">Transcoding progress</th>
|
<th width="200">Total transcoding progress</th>
|
||||||
|
<th width="200">Default resolution transcoding progress</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for media_entry in entries %}
|
{% for media_entry in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -84,6 +86,11 @@ Show:
|
|||||||
{% else %}
|
{% else %}
|
||||||
<td>Unknown</td>
|
<td>Unknown</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if media_entry.main_transcoding_progress %}
|
||||||
|
<td>{{ media_entry.main_transcoding_progress }}%</td>
|
||||||
|
{% else %}
|
||||||
|
<td>Unknown</td>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -93,3 +100,4 @@ Show:
|
|||||||
<p><em>{% trans %}You have not uploaded anything yet!{% endtrans %}</em></p>
|
<p><em>{% trans %}You have not uploaded anything yet!{% endtrans %}</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -48,8 +48,9 @@ def test_setup_celery_from_config():
|
|||||||
assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float)
|
assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float)
|
||||||
assert fake_celery_module.CELERY_RESULT_PERSISTENT is True
|
assert fake_celery_module.CELERY_RESULT_PERSISTENT is True
|
||||||
assert fake_celery_module.CELERY_IMPORTS == [
|
assert fake_celery_module.CELERY_IMPORTS == [
|
||||||
'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task', \
|
'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task',
|
||||||
'mediagoblin.notifications.task', 'mediagoblin.submit.task']
|
'mediagoblin.notifications.task', 'mediagoblin.submit.task',
|
||||||
|
'mediagoblin.media_types.video.processing']
|
||||||
assert fake_celery_module.CELERY_RESULT_BACKEND == 'database'
|
assert fake_celery_module.CELERY_RESULT_BACKEND == 'database'
|
||||||
assert fake_celery_module.CELERY_RESULT_DBURI == (
|
assert fake_celery_module.CELERY_RESULT_DBURI == (
|
||||||
'sqlite:///' +
|
'sqlite:///' +
|
||||||
|
@ -47,17 +47,27 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
import webtest.forms
|
import webtest.forms
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
try:
|
||||||
|
import mock
|
||||||
|
except ImportError:
|
||||||
|
import unittest.mock as mock
|
||||||
|
|
||||||
import six.moves.urllib.parse as urlparse
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
|
from celery import Signature
|
||||||
from mediagoblin.tests.tools import (
|
from mediagoblin.tests.tools import (
|
||||||
fixture_add_user, fixture_add_collection, get_app)
|
fixture_add_user, fixture_add_collection, get_app)
|
||||||
from mediagoblin import mg_globals
|
from mediagoblin import mg_globals
|
||||||
from mediagoblin.db.models import MediaEntry, User, LocalUser, Activity
|
from mediagoblin.db.models import MediaEntry, User, LocalUser, Activity, MediaFile
|
||||||
from mediagoblin.db.base import Session
|
from mediagoblin.db.base import Session
|
||||||
from mediagoblin.tools import template
|
from mediagoblin.tools import template
|
||||||
from mediagoblin.media_types.image import ImageMediaManager
|
from mediagoblin.media_types.image import ImageMediaManager
|
||||||
from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites
|
from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites
|
||||||
|
from mediagoblin.media_types.video.processing import (
|
||||||
|
VideoProcessingManager, main_task, complementary_task, group,
|
||||||
|
processing_cleanup, CommonVideoProcessor)
|
||||||
|
from mediagoblin.media_types.video.util import ACCEPTED_RESOLUTIONS
|
||||||
|
from mediagoblin.submit.lib import new_upload_entry, run_process_media
|
||||||
|
|
||||||
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
|
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
|
||||||
BIG_BLUE, GOOD_PDF, GPS_JPG, MED_PNG, BIG_PNG
|
BIG_BLUE, GOOD_PDF, GPS_JPG, MED_PNG, BIG_PNG
|
||||||
@ -101,6 +111,16 @@ def pdf_plugin_app(request):
|
|||||||
'mediagoblin.tests',
|
'mediagoblin.tests',
|
||||||
'test_mgoblin_app_pdf.ini'))
|
'test_mgoblin_app_pdf.ini'))
|
||||||
|
|
||||||
|
def get_sample_entry(user, media_type):
|
||||||
|
entry = new_upload_entry(user)
|
||||||
|
entry.media_type = media_type
|
||||||
|
entry.title = 'testentry'
|
||||||
|
entry.description = u""
|
||||||
|
entry.license = None
|
||||||
|
entry.media_metadata = {}
|
||||||
|
entry.save()
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
class BaseTestSubmission:
|
class BaseTestSubmission:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -523,6 +543,7 @@ class TestSubmissionVideo(BaseTestSubmission):
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self, video_plugin_app):
|
def setup(self, video_plugin_app):
|
||||||
self.test_app = video_plugin_app
|
self.test_app = video_plugin_app
|
||||||
|
self.media_type = 'mediagoblin.media_types.video'
|
||||||
|
|
||||||
# TODO: Possibly abstract into a decorator like:
|
# TODO: Possibly abstract into a decorator like:
|
||||||
# @as_authenticated_user('chris')
|
# @as_authenticated_user('chris')
|
||||||
@ -536,6 +557,179 @@ class TestSubmissionVideo(BaseTestSubmission):
|
|||||||
with create_av(make_video=True) as path:
|
with create_av(make_video=True) as path:
|
||||||
self.check_normal_upload('Video', path)
|
self.check_normal_upload('Video', path)
|
||||||
|
|
||||||
|
media = mg_globals.database.MediaEntry.query.filter_by(
|
||||||
|
title=u'Video').first()
|
||||||
|
|
||||||
|
video_config = mg_globals.global_config['plugins'][self.media_type]
|
||||||
|
for each_res in video_config['available_resolutions']:
|
||||||
|
assert (('webm_' + str(each_res)) in media.media_files)
|
||||||
|
|
||||||
|
@pytest.mark.skipif(SKIP_VIDEO,
|
||||||
|
reason="Dependencies for video not met")
|
||||||
|
def test_get_all_media(self, video_plugin_app):
|
||||||
|
"""Test if the get_all_media function returns sensible things
|
||||||
|
"""
|
||||||
|
with create_av(make_video=True) as path:
|
||||||
|
self.check_normal_upload('testgetallmedia', path)
|
||||||
|
|
||||||
|
media = mg_globals.database.MediaEntry.query.filter_by(
|
||||||
|
title=u'testgetallmedia').first()
|
||||||
|
result = media.get_all_media()
|
||||||
|
video_config = mg_globals.global_config['plugins'][self.media_type]
|
||||||
|
|
||||||
|
for media_file in result:
|
||||||
|
# checking that each returned media file list has 3 elements
|
||||||
|
assert len(media_file) == 3
|
||||||
|
|
||||||
|
# result[0][0] is the video label of the first video in the list
|
||||||
|
if result[0][0] == 'default':
|
||||||
|
media_file = MediaFile.query.filter_by(media_entry=media.id,
|
||||||
|
name=('webm_video')).first()
|
||||||
|
# only one media file has to be present in this case
|
||||||
|
assert len(result) == 1
|
||||||
|
# check dimensions of media_file
|
||||||
|
assert result[0][1] == list(ACCEPTED_RESOLUTIONS['webm'])
|
||||||
|
# check media_file path
|
||||||
|
assert result[0][2] == media_file.file_path
|
||||||
|
else:
|
||||||
|
assert len(result) == len(video_config['available_resolutions'])
|
||||||
|
for i in range(len(video_config['available_resolutions'])):
|
||||||
|
media_file = MediaFile.query.filter_by(media_entry=media.id,
|
||||||
|
name=('webm_{0}'.format(str(result[i][0])))).first()
|
||||||
|
# check media_file label
|
||||||
|
assert result[i][0] == video_config['available_resolutions'][i]
|
||||||
|
# check dimensions of media_file
|
||||||
|
assert result[i][1] == list(ACCEPTED_RESOLUTIONS[
|
||||||
|
video_config['available_resolutions'][i]])
|
||||||
|
# check media_file path
|
||||||
|
assert result[i][2] == media_file.file_path
|
||||||
|
|
||||||
|
@mock.patch('mediagoblin.media_types.video.processing.processing_cleanup.signature')
|
||||||
|
@mock.patch('mediagoblin.media_types.video.processing.complementary_task.signature')
|
||||||
|
@mock.patch('mediagoblin.media_types.video.processing.main_task.signature')
|
||||||
|
def test_celery_tasks(self, mock_main_task, mock_comp_task, mock_cleanup):
|
||||||
|
|
||||||
|
# create a new entry and get video manager
|
||||||
|
entry = get_sample_entry(self.our_user(), self.media_type)
|
||||||
|
manager = VideoProcessingManager()
|
||||||
|
|
||||||
|
# prepare things for testing
|
||||||
|
video_config = mg_globals.global_config['plugins'][entry.media_type]
|
||||||
|
def_res = video_config['default_resolution']
|
||||||
|
priority_num = len(video_config['available_resolutions']) + 1
|
||||||
|
main_priority = priority_num
|
||||||
|
calls = []
|
||||||
|
reprocess_info = {
|
||||||
|
'vorbis_quality': None,
|
||||||
|
'vp8_threads': None,
|
||||||
|
'thumb_size': None,
|
||||||
|
'vp8_quality': None
|
||||||
|
}
|
||||||
|
for comp_res in video_config['available_resolutions']:
|
||||||
|
if comp_res != def_res:
|
||||||
|
priority_num += -1
|
||||||
|
calls.append(
|
||||||
|
mock.call(args=(entry.id, comp_res, ACCEPTED_RESOLUTIONS[comp_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# call workflow method
|
||||||
|
manager.workflow(entry, feed_url=None, reprocess_action='initial')
|
||||||
|
|
||||||
|
# test section
|
||||||
|
mock_main_task.assert_called_once_with(args=(entry.id, def_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[def_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=main_priority, immutable=True)
|
||||||
|
mock_comp_task.assert_has_calls(calls)
|
||||||
|
mock_cleanup.assert_called_once_with(args=(entry.id,), queue='default',
|
||||||
|
immutable=True)
|
||||||
|
assert entry.state == u'processing'
|
||||||
|
|
||||||
|
# delete the entry
|
||||||
|
entry.delete()
|
||||||
|
|
||||||
|
def test_workflow(self):
|
||||||
|
entry = get_sample_entry(self.our_user(), self.media_type)
|
||||||
|
manager = VideoProcessingManager()
|
||||||
|
wf = manager.workflow(entry, feed_url=None, reprocess_action='initial')
|
||||||
|
assert type(wf) == tuple
|
||||||
|
assert len(wf) == 2
|
||||||
|
assert isinstance(wf[0], group)
|
||||||
|
assert isinstance(wf[1], Signature)
|
||||||
|
|
||||||
|
# more precise testing
|
||||||
|
video_config = mg_globals.global_config['plugins'][entry.media_type]
|
||||||
|
def_res = video_config['default_resolution']
|
||||||
|
priority_num = len(video_config['available_resolutions']) + 1
|
||||||
|
reprocess_info = {
|
||||||
|
'vorbis_quality': None,
|
||||||
|
'vp8_threads': None,
|
||||||
|
'thumb_size': None,
|
||||||
|
'vp8_quality': None
|
||||||
|
}
|
||||||
|
tasks_list = [main_task.signature(args=(entry.id, def_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[def_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)]
|
||||||
|
for comp_res in video_config['available_resolutions']:
|
||||||
|
if comp_res != def_res:
|
||||||
|
priority_num += -1
|
||||||
|
tasks_list.append(
|
||||||
|
complementary_task.signature(args=(entry.id, comp_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[comp_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)
|
||||||
|
)
|
||||||
|
transcoding_tasks = group(tasks_list)
|
||||||
|
cleanup_task = processing_cleanup.signature(args=(entry.id,),
|
||||||
|
queue='default', immutable=True)
|
||||||
|
assert wf[0] == transcoding_tasks
|
||||||
|
assert wf[1] == cleanup_task
|
||||||
|
entry.delete()
|
||||||
|
|
||||||
|
@mock.patch('mediagoblin.submit.lib.ProcessMedia.apply_async')
|
||||||
|
@mock.patch('mediagoblin.submit.lib.chord')
|
||||||
|
def test_celery_chord(self, mock_chord, mock_process_media):
|
||||||
|
entry = get_sample_entry(self.our_user(), self.media_type)
|
||||||
|
|
||||||
|
# prepare things for testing
|
||||||
|
video_config = mg_globals.global_config['plugins'][entry.media_type]
|
||||||
|
def_res = video_config['default_resolution']
|
||||||
|
priority_num = len(video_config['available_resolutions']) + 1
|
||||||
|
reprocess_info = {
|
||||||
|
'vorbis_quality': None,
|
||||||
|
'vp8_threads': None,
|
||||||
|
'thumb_size': None,
|
||||||
|
'vp8_quality': None
|
||||||
|
}
|
||||||
|
tasks_list = [main_task.signature(args=(entry.id, def_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[def_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)]
|
||||||
|
for comp_res in video_config['available_resolutions']:
|
||||||
|
if comp_res != def_res:
|
||||||
|
priority_num += -1
|
||||||
|
tasks_list.append(
|
||||||
|
complementary_task.signature(args=(entry.id, comp_res,
|
||||||
|
ACCEPTED_RESOLUTIONS[comp_res]),
|
||||||
|
kwargs=reprocess_info, queue='default',
|
||||||
|
priority=priority_num, immutable=True)
|
||||||
|
)
|
||||||
|
transcoding_tasks = group(tasks_list)
|
||||||
|
run_process_media(entry)
|
||||||
|
mock_chord.assert_called_once_with(transcoding_tasks)
|
||||||
|
entry.delete()
|
||||||
|
|
||||||
|
def test_accepted_files(self):
|
||||||
|
entry = get_sample_entry(self.our_user(), 'mediagoblin.media_types.video')
|
||||||
|
manager = VideoProcessingManager()
|
||||||
|
processor = CommonVideoProcessor(manager, entry)
|
||||||
|
acceptable_files = ['original, best_quality', 'webm_144p', 'webm_360p',
|
||||||
|
'webm_480p', 'webm_720p', 'webm_1080p', 'webm_video']
|
||||||
|
assert processor.acceptable_files == acceptable_files
|
||||||
|
|
||||||
|
|
||||||
class TestSubmissionAudio(BaseTestSubmission):
|
class TestSubmissionAudio(BaseTestSubmission):
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -591,3 +785,4 @@ class TestSubmissionPDF(BaseTestSubmission):
|
|||||||
**self.upload_data(GOOD_PDF))
|
**self.upload_data(GOOD_PDF))
|
||||||
self.check_url(response, '/u/{0}/'.format(self.our_user().username))
|
self.check_url(response, '/u/{0}/'.format(self.our_user().username))
|
||||||
assert 'mediagoblin/user_pages/user.html' in context
|
assert 'mediagoblin/user_pages/user.html' in context
|
||||||
|
|
||||||
|
@ -30,7 +30,9 @@ Gst.init(None)
|
|||||||
|
|
||||||
from mediagoblin.media_types.video.transcoders import (capture_thumb,
|
from mediagoblin.media_types.video.transcoders import (capture_thumb,
|
||||||
VideoTranscoder)
|
VideoTranscoder)
|
||||||
|
from mediagoblin.media_types.video.util import ACCEPTED_RESOLUTIONS
|
||||||
from mediagoblin.media_types.tools import discover
|
from mediagoblin.media_types.tools import discover
|
||||||
|
from mediagoblin.tests.tools import get_app
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def create_data(suffix=None, make_audio=False):
|
def create_data(suffix=None, make_audio=False):
|
||||||
@ -114,6 +116,7 @@ def test_transcoder():
|
|||||||
transcoder = VideoTranscoder()
|
transcoder = VideoTranscoder()
|
||||||
transcoder.transcode(
|
transcoder.transcode(
|
||||||
video_name, result_name,
|
video_name, result_name,
|
||||||
|
'480p', 1,
|
||||||
vp8_quality=8,
|
vp8_quality=8,
|
||||||
vp8_threads=0, # autodetect
|
vp8_threads=0, # autodetect
|
||||||
vorbis_quality=0.3,
|
vorbis_quality=0.3,
|
||||||
@ -124,9 +127,22 @@ def test_transcoder():
|
|||||||
transcoder = VideoTranscoder()
|
transcoder = VideoTranscoder()
|
||||||
transcoder.transcode(
|
transcoder.transcode(
|
||||||
video_name, result_name,
|
video_name, result_name,
|
||||||
|
'480p', 1,
|
||||||
vp8_quality=8,
|
vp8_quality=8,
|
||||||
vp8_threads=0, # autodetect
|
vp8_threads=0, # autodetect
|
||||||
vorbis_quality=0.3,
|
vorbis_quality=0.3,
|
||||||
dimensions=(640, 640))
|
dimensions=(640, 640))
|
||||||
assert len(discover(result_name).get_video_streams()) == 1
|
assert len(discover(result_name).get_video_streams()) == 1
|
||||||
assert len(discover(result_name).get_audio_streams()) == 1
|
assert len(discover(result_name).get_audio_streams()) == 1
|
||||||
|
|
||||||
|
def test_accepted_resolutions():
|
||||||
|
accepted_resolutions = {
|
||||||
|
'144p': (256, 144),
|
||||||
|
'240p': (352, 240),
|
||||||
|
'360p': (480, 360),
|
||||||
|
'480p': (858, 480),
|
||||||
|
'720p': (1280, 720),
|
||||||
|
'1080p': (1920, 1080),
|
||||||
|
'webm': (640, 640),
|
||||||
|
}
|
||||||
|
assert accepted_resolutions == ACCEPTED_RESOLUTIONS
|
||||||
|
@ -66,8 +66,7 @@ def user_home(request, page):
|
|||||||
{'user': user})
|
{'user': user})
|
||||||
|
|
||||||
cursor = MediaEntry.query.\
|
cursor = MediaEntry.query.\
|
||||||
filter_by(actor = user.id,
|
filter_by(actor = user.id).order_by(MediaEntry.created.desc())
|
||||||
state = u'processed').order_by(MediaEntry.created.desc())
|
|
||||||
|
|
||||||
pagination = Pagination(page, cursor)
|
pagination = Pagination(page, cursor)
|
||||||
media_entries = pagination()
|
media_entries = pagination()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user