Skip to content

Commit

Permalink
Merge pull request #15 from idsvn/master
Browse files Browse the repository at this point in the history
Update Video Editor
  • Loading branch information
ride90 authored Nov 26, 2019
2 parents 799770d + e9c4c8e commit 9bc425a
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 209 deletions.
14 changes: 3 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,7 @@ where `5d7b841764c598157d53ef4a` is project's `_id` you want to make a duplicate
curl -X PUT \
http://0.0.0.0:5050/projects/5d7a35a04be797ba845e7871 \
-d '{
"trim": {
"start": 2,
"end": 5
}
"trim": "2,5"
}'
```
where `2` and `5` are seconds.
Expand Down Expand Up @@ -169,12 +166,7 @@ where `480` is width you want to scale video to.
curl -X PUT \
http://0.0.0.0:5050/projects/5d7a35a04be797ba845e7871 \
-d '{
"crop": {
"height": 180,
"width": 320,
"x": 0,
"y": 0
}
"crop": "0,0,180,320"
}'
```
where `width` and `height` are respectively width and height of capturing area,
Expand All @@ -193,7 +185,7 @@ curl -X GET 'http://0.0.0.0:5050/projects/5d7b98f52fac91d2e1ad7512/thumbnails?ty
where `position` is a position in the video (seconds) used to capture a thumbnail.

You can also specify optional `crop` param if you want to crop a preview thumbnail, just add
`crop={ "height": 180, "width": 320, "x": 0, "y": 0 }`.
`crop="0,0,180,320"`.
Example:
```bash
curl -X GET \
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
max-line-length=120
exclude=env,bin,lib,include,src,docs
exclude=.tox,__pycache__,env,bin,include,docs,*.pyc
ignore=F811,D200,D202,D205,D400,D401,D100,D101,D102,D103,D104,D105,D107,W503,W504,W605,F401,E261,F841,D413
# W504, W605, F401, E261 and F841 are temporarly ignored, due to recent changes in flake8
130 changes: 29 additions & 101 deletions src/videoserver/apps/projects/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import logging
import os
import re
from ast import literal_eval
from datetime import datetime

import bson
Expand All @@ -15,8 +14,8 @@
from videoserver.lib.video_editor import get_video_editor
from videoserver.lib.views import MethodView
from videoserver.lib.utils import (
add_urls, create_file_name, get_request_address, json_response,
paginate, save_activity_log, storage2response, validate_document
add_urls, create_file_name, get_request_address, json_response, paginate, save_activity_log, storage2response,
validate_document, coerce_crop_str_to_dict, coerce_trim_str_to_dict
)

from . import bp
Expand Down Expand Up @@ -167,7 +166,7 @@ def post(self):
},
'thumbnails': {
'timeline': [],
'preview': None
'preview': {},
}
}

Expand Down Expand Up @@ -338,20 +337,11 @@ class RetrieveEditDestroyProject(MethodView):
def schema_edit(self):
return {
'trim': {
'type': 'dict',
'required': False,
'schema': {
'start': {
'type': 'float',
'min': 0,
'required': True
},
'end': {
'type': 'float',
'min': 1,
'required': True
},
}
'regex': r'^\d+\.?\d*,\d+\.?\d*$',
'coerce': coerce_trim_str_to_dict,
'min_trim_start': 0,
'min_trim_end': 1
},
'rotate': {
'type': 'integer',
Expand All @@ -365,33 +355,11 @@ def schema_edit(self):
'required': False
},
'crop': {
'type': 'dict',
'required': False,
'empty': True,
'schema': {
'width': {
'type': 'integer',
'min': app.config.get('MIN_VIDEO_WIDTH'),
'max': app.config.get('MAX_VIDEO_WIDTH'),
'required': True
},
'height': {
'type': 'integer',
'min': app.config.get('MIN_VIDEO_HEIGHT'),
'max': app.config.get('MAX_VIDEO_HEIGHT'),
'required': True
},
'x': {
'type': 'integer',
'required': True,
'min': 0
},
'y': {
'type': 'integer',
'required': True,
'min': 0
}
}
'regex': r'^\d+,\d+,\d+,\d+$',
'coerce': coerce_crop_str_to_dict,
'allow_crop_width': [app.config.get('MIN_VIDEO_WIDTH'), app.config.get('MAX_VIDEO_WIDTH')],
'allow_crop_height': [app.config.get('MIN_VIDEO_HEIGHT'), app.config.get('MAX_VIDEO_HEIGHT')]
}
}

Expand Down Expand Up @@ -520,29 +488,11 @@ def put(self, project_id):
type: object
properties:
trim:
type: object
properties:
start:
type: integer
example: 5
end:
type: integer
example: 10
type: string
example: 5.1,10.5
crop:
type: object
properties:
width:
type: integer
example: 480
height:
type: integer
example: 360
x:
type: integer
example: 10
y:
type: integer
example: 10
type: string
example: 480,360,10,10
rotate:
type: integer
enum: [-270, -180, -90, 90, 180, 270]
Expand Down Expand Up @@ -600,9 +550,10 @@ def put(self, project_id):
{"start": [f"trimmed video must be at least {app.config.get('MIN_TRIM_DURATION')} seconds"]}
]})
elif document['trim']['end'] > metadata['duration']:
raise BadRequest({"trim": [
{"end": [f"outside of initial video's length"]}
]})
document['trim']['end'] = metadata['duration']
logger.info(
f"Trimmed video endtime greater than video duration, update it to equal duration, "
f"ID: {self.project['_id']}")
elif document['trim']['start'] == 0 and document['trim']['end'] == metadata['duration']:
raise BadRequest({"trim": [
{"end": ["trim is duplicating an entire video"]}
Expand Down Expand Up @@ -804,7 +755,7 @@ def post(self, project_id):
child_project['version'] += 1
child_project['thumbnails'] = {
'timeline': [],
'preview': None
'preview': {}
}
app.mongo.db.projects.insert_one(child_project)

Expand Down Expand Up @@ -934,32 +885,11 @@ def schema_thumbnails(self):
'coerce': float,
},
'crop': {
'type': 'dict',
'coerce': literal_eval, # crop args are a string represent of a dict
'required': False,
'empty': True,
'schema': {
'width': {
'type': 'integer',
'required': True,
'min': app.config.get('MIN_VIDEO_WIDTH'),
},
'height': {
'type': 'integer',
'required': True,
'min': app.config.get('MIN_VIDEO_HEIGHT'),
},
'x': {
'type': 'integer',
'required': True,
'min': 0
},
'y': {
'type': 'integer',
'required': True,
'min': 0
}
}
'regex': r'^\d+,\d+,\d+,\d+$',
'coerce': coerce_crop_str_to_dict,
'allow_crop_width': [app.config.get('MIN_VIDEO_WIDTH'), app.config.get('MAX_VIDEO_WIDTH')],
'allow_crop_height': [app.config.get('MIN_VIDEO_HEIGHT'), app.config.get('MAX_VIDEO_HEIGHT')]
},
'rotate': {
'type': 'integer',
Expand Down Expand Up @@ -998,7 +928,7 @@ def get(self, project_id):
in: query
type: json
description: Crop rules apply to preview thumbnail. Used only when `type` is `preview`.
default: "{'width': 720, 'height': 360, 'x': 0, 'y':0}"
default: "0,0,720,360"
- name: rotate
in: query
type: integer
Expand Down Expand Up @@ -1226,15 +1156,13 @@ def _get_preview_thumbnail(self, position, crop, rotate):
raise BadRequest({"crop": [{"width": ["crop's frame is outside a video's frame"]}]})
elif crop['y'] + crop['height'] > self.project['metadata']['height']:
raise BadRequest({"crop": [{"height": ["crop's frame is outside a video's frame"]}]})

# validate position param
if self.project['metadata']['duration'] < position:
position = self.project['metadata']['duration']
logger.info(f"Postition greater than video duration, Update it equal duration, ID: {self.project['_id']}")
# resource is busy
if self.project['processing']['thumbnail_preview']:
raise Conflict({"processing": ["Task get preview thumbnails video is still processing"]})
elif self.project['metadata']['duration'] < position:
raise BadRequest({
'position': [f"Requested position: '{position}' is more than video's duration: "
f"'{self.project['metadata']['duration']}'."]
})
else:
# set processing flag
self.project = app.mongo.db.projects.find_one_and_update(
Expand Down
9 changes: 6 additions & 3 deletions src/videoserver/lib/storage/file_system_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def get_range(self, storage_id, start, length):

return media_file

def put(self, content, filename, project_id=None, asset_type='project', storage_id=None, content_type=None):
def put(self, content, filename, project_id=None, asset_type='project', storage_id=None, content_type=None,
override=True):
"""
Save file into a fs storage.
Expand Down Expand Up @@ -110,7 +111,9 @@ def put(self, content, filename, project_id=None, asset_type='project', storage_
file_path = self._get_file_path(storage_id)
# check if file exists
if os.path.exists(file_path):
raise Exception(f'File {file_path} already exists, use "replace" method instead.')
if not override:
raise Exception(f'File {file_path} already exists, use "replace" method instead.')
self.replace(content, storage_id)

# check if dir exists, if not create it
file_dir = os.path.dirname(file_path)
Expand Down Expand Up @@ -181,4 +184,4 @@ def delete_dir(self, storage_id):
shutil.rmtree(dir_path)
logger.info(f"Removed '{dir_path}' from fs storage")
else:
logger.warning(f"Directory '{dir_path}' was not found in fs storage.")
logger.warning(f"Directory '{dir_path}' was not found in fs storage.")
66 changes: 62 additions & 4 deletions src/videoserver/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def _handle_doc(doc):
_external=True
)

if doc['thumbnails']['preview']:
if doc['thumbnails']['preview'] or doc['processing']['thumbnail_preview']:
doc['thumbnails']['preview']['url'] = url_for(
'projects.get_raw_preview_thumbnail',
project_id=doc['_id'],
Expand Down Expand Up @@ -125,6 +125,22 @@ def save_activity_log(action, project_id, payload=None):
})


def coerce_crop_str_to_dict(value):
"""
Use for coerce crop value from str (x,y,w,h) to dict
"""
x, y, width, height = [int(item) for item in value.split(',')]
return {"x": x, "y": y, "width": width, "height": height}


def coerce_trim_str_to_dict(value):
"""
Use for coerce trim value from str (start,end) to dict
"""
start, end = [float(item) for item in value.split(',')]
return {"start": start, "end": end}


def validate_document(document, schema, **kwargs):
"""
Validate `document` against provided `schema`
Expand All @@ -138,12 +154,54 @@ def validate_document(document, schema, **kwargs):
:raise: `BadRequest` if `document` is not valid
"""

validator = Validator(schema, **kwargs)
validator = VideoValidator(schema, **kwargs)
if not validator.validate(document):
raise BadRequest(validator.errors)
return validator.document


class VideoValidator(Validator):
def _validate_allow_crop_width(self, limit, field, value):
"""Test allowed crop width range
The rule's arguments are validated against this schema:
{'min': 'limit[0]', 'max': 'limit[1]'}
"""
if limit and len(limit) == 2:
wmin, wmax = limit
if value['width'] < wmin:
self._error(field, "width is lesser than minimum allowed crop width")
if value['width'] > wmax:
self._error(field, "width is greater than maximum allowed crop width")

def _validate_allow_crop_height(self, limit, field, value):
"""Test allowed crop height range
The rule's arguments are validated against this schema:
{'min': 'limit[0]', 'max': 'limit[1]'}
"""
if limit and len(limit) == 2:
hmin, hmax = limit
if value['height'] < hmin:
self._error(field, "height is lesser than minimum allowed crop height")
if value['height'] > hmax:
self._error(field, "height is greater than maximum allowed crop height")

def _validate_min_trim_start(self, min_trim, field, value):
"""Test minimum allowed trim start value
The rule's arguments are validated against this schema:
{'min': 'min_trim'}
"""
if min_trim is not None and value['start'] < min_trim:
self._error(field, "start time must be greater than %s" % min_trim)

def _validate_min_trim_end(self, min_trim, field, value):
"""Test minium allowed trim end value
The rule's arguments are validated against this schema:
{'min': 'min_trim'}
"""
if min_trim is not None and value['end'] < min_trim:
self._error(field, "end time must be greater than %s" % min_trim)


def get_request_address(request_headers):
return request_headers.get('HTTP_X_FORWARDED_FOR') or request_headers.get('REMOTE_ADDR')

Expand All @@ -170,10 +228,10 @@ def create_temp_file(file_stream, suffix=None):
def storage2response(storage_id, headers=None, status=200, start=None, length=None):
"""
Fetch binary using `storage_id` and return http response.
:param storage_id: Unique storage id
:type storage_id: str
:param headers: header for response
:param headers: header for response
:type headers: dict
:param status: http status code
:type status: int
Expand Down
2 changes: 1 addition & 1 deletion src/videoserver/lib/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from werkzeug.datastructures import FileStorage as WerkzeugFileStorage



class Validator(DefaultValidator):
"""
Custom validator with additional types
"""

types_mapping = DefaultValidator.types_mapping.copy()
types_mapping['filestorage'] = TypeDefinition('filestorage', (WerkzeugFileStorage,), ())
Loading

0 comments on commit 9bc425a

Please sign in to comment.