Unverified Commit 15e1f50b by Gregory Martin Committed by GitHub

Merge pull request #47 from edx/deployment_debug

Debugs from Deployment 16.10.17
parents c0d7ab65 01f64063
......@@ -16,3 +16,5 @@ reports/
.cache/
VEDA/settings/private.py
youtube_callback/static_files/youtubekey
[pep8]
max-line-length = 120
exclude = dependencies, watchdog.py, veda_deliver_xuetang.py, scripts, settings, migrations
exclude = dependencies, watchdog.py, scripts, settings, migrations
"""
Veda Delivery unit tests
"""
import os
import unittest
from django.test import TestCase
import responses
from control.veda_deliver import VedaDelivery
from control.veda_file_ingest import VideoProto
from mock import PropertyMock, patch
from VEDA.utils import get_config
from VEDA_OS01.models import URL, Course, Destination, Encode, Video
CONFIG_DATA = get_config('test_config.yaml')
class VedaDeliverRunTest(TestCase):
"""
Deliver Run Tests
"""
def setUp(self):
self.veda_id = 'XXXXXXXX2014-V00TES1'
self.encode_profile = 'hls'
self.course = Course.objects.create(
institution='XXX',
edx_classid='XXXXX',
course_name=u'Intro to VEDA',
local_storedir=u'This/Is/A/testID'
)
self.video = Video.objects.create(
inst_class=self.course,
edx_id=self.veda_id,
client_title='Test Video',
video_orig_duration='00:00:10.09', # This is known
pk=1
)
self.destination = Destination.objects.create(
destination_name='TEST'
)
self.encode = Encode.objects.create(
encode_destination=self.destination,
profile_active=True,
encode_suffix='HLS',
product_spec=self.encode_profile,
encode_filetype='HLS'
)
self.deliver_instance = VedaDelivery(
veda_id=self.veda_id,
encode_profile=self.encode_profile,
CONFIG_DATA=CONFIG_DATA
)
@patch('control.veda_val.VALAPICall._AUTH', PropertyMock(return_value=lambda: CONFIG_DATA))
@responses.activate
def test_run(self):
"""
Test of HLS run-through function
"""
# VAL Patching
responses.add(responses.POST, CONFIG_DATA['val_token_url'], '{"access_token": "1234567890"}', status=200)
responses.add(
responses.GET,
CONFIG_DATA['val_api_url'] + '/XXXXXXXX2014-V00TES1',
status=200,
json={'error': 'null', 'courses': [], 'encoded_videos': []}
)
responses.add(responses.PUT, CONFIG_DATA['val_api_url'] + '/XXXXXXXX2014-V00TES1', status=200)
self.VP = VideoProto(
client_title='Test Video',
veda_id=self.veda_id
)
self.encoded_file = '{file_name}_{suffix}.{ext}'.format(
file_name=self.veda_id,
suffix=self.encode.encode_suffix,
ext=self.encode.encode_filetype
)
self.deliver_instance.run()
# Assertions
self.assertEqual(self.deliver_instance.video_proto.val_id, 'XXXXXXXX2014-V00TES1')
self.assertEqual(self.deliver_instance.video_proto.veda_id, self.veda_id)
self.assertEqual(self.deliver_instance.video_proto.duration, 10.09)
self.assertEqual(self.deliver_instance.video_proto.s3_filename, None)
self.assertEqual(self.deliver_instance.encode_query, self.encode)
self.assertEqual(self.deliver_instance.encoded_file, '/'.join((self.veda_id, self.veda_id + '.m3u8')))
self.assertEqual(self.deliver_instance.status, 'Complete')
self.assertEqual(
self.deliver_instance.endpoint_url,
'/'.join((
CONFIG_DATA['edx_cloudfront_prefix'],
self.veda_id,
self.veda_id + '.m3u8'
))
)
def test_intake(self):
"""
Framework for intake testing
"""
self.deliver_instance._INFORM_INTAKE()
self.encoded_file = '{file_name}_{suffix}.{ext}'.format(
file_name=self.veda_id,
suffix=self.encode.encode_suffix,
ext=self.encode.encode_filetype
)
self.assertEqual(self.deliver_instance.encoded_file, self.encoded_file)
self.assertEqual(
self.deliver_instance.hotstore_url,
'/'.join((
'https:/',
's3.amazonaws.com',
CONFIG_DATA['veda_deliverable_bucket'],
self.encoded_file
))
)
@unittest.skip('Skipping this test due to unavailability of ffprobe req')
def test_validate(self):
"""
Simple test of validation call from deliver, not a full test of validation function
"""
self.deliver_instance.node_work_directory = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'test_files'
)
self.deliver_instance.encoded_file = 'OVTESTFILE_01.mp4'
self.assertTrue(self.deliver_instance._VALIDATE())
def test_determine_status(self):
"""
Test Video Status determiner
"""
self.deliver_instance.video_query = self.video
self.assertEqual(self.deliver_instance._DETERMINE_STATUS(), 'Progress')
self.url = URL.objects.create(
encode_profile=self.encode,
videoID=self.video,
encode_url='Test_URL'
)
self.assertEqual(self.deliver_instance._DETERMINE_STATUS(), 'Complete')
def test_validate_url(self):
"""
Test URL Validator
"""
self.assertFalse(self.deliver_instance._VALIDATE_URL())
self.deliver_instance.endpoint_url = 'https://edx.org'
self.assertTrue(self.deliver_instance._VALIDATE_URL())
@patch('control.veda_val.VALAPICall._AUTH', PropertyMock(return_value=lambda: CONFIG_DATA))
@responses.activate
def test_update_data(self):
"""
Run test of VAL status / call
"""
# VAL Patching
responses.add(responses.POST, CONFIG_DATA['val_token_url'], '{"access_token": "1234567890"}', status=200)
responses.add(
responses.GET,
CONFIG_DATA['val_api_url'] + '/XXXXXXXX2014-V00TES1',
status=200,
json={'error': 'null', 'courses': [], 'encoded_videos': []}
)
responses.add(responses.PUT, CONFIG_DATA['val_api_url'] + '/XXXXXXXX2014-V00TES1', status=200)
self.VP = VideoProto(
client_title='Test Video',
veda_id=self.veda_id
)
self.deliver_instance.video_query = self.video
# No Update
self.deliver_instance._UPDATE_DATA()
self.assertEqual(self.deliver_instance.val_status, None)
# Incomplete
self.deliver_instance.status = 'Garbled'
self.deliver_instance._UPDATE_DATA()
self.assertEqual(self.deliver_instance.val_status, 'transcode_active')
# Complete
self.deliver_instance.status = 'Complete'
self.deliver_instance._UPDATE_DATA()
self.assertEqual(self.deliver_instance.val_status, 'file_complete')
"""
VEDA Delivery:
Determine the destination and upload to the appropriate
endpoint via the custom methods
"""
import datetime
import logging
import shutil
......@@ -6,12 +11,13 @@ from os.path import expanduser
import boto
import boto.s3
from boto.s3.connection import S3Connection
import requests
from boto.exception import S3ResponseError
from boto.exception import S3ResponseError, NoAuthHandlerFound
from boto.s3.key import Key
from django.core.urlresolvers import reverse
import veda_deliver_xuetang
from control_env import *
from veda_deliver_cielo import Cielo24Transcript
from veda_deliver_youtube import DeliverYoutube
......@@ -22,43 +28,28 @@ from VEDA.utils import build_url, extract_course_org, get_config
from veda_utils import ErrorObject, Metadata, Output, VideoProto
from veda_val import VALAPICall
from veda_video_validation import Validation
from watchdog import Watchdog
try:
from control.veda_deliver_3play import ThreePlayMediaClient
except ImportError:
from veda_deliver_3play import ThreePlayMediaClient
LOGGER = logging.getLogger(__name__)
try:
boto.config.add_section('Boto')
except:
pass
boto.config.set('Boto', 'http_socket_timeout', '100')
"""
VEDA Delivery class - determine the destination
and upload to the appropriate endpoint via the approp. methods
"""
homedir = expanduser("~")
watchdog_time = 10.0
class VedaDelivery:
def __init__(self, veda_id, encode_profile, **kwargs):
self.veda_id = veda_id
self.encode_profile = encode_profile
self.auth_dict = get_config()
self.auth_dict = kwargs.get('CONFIG_DATA', get_config())
# Internal Methods
self.video_query = None
self.encode_query = None
......@@ -68,6 +59,7 @@ class VedaDelivery:
self.status = None
self.endpoint_url = None
self.video_proto = None
self.val_status = None
def run(self):
"""
......@@ -75,29 +67,8 @@ class VedaDelivery:
throw error if method is not extant
"""
if self.encode_profile == 'hls':
self.video_query = Video.objects.filter(edx_id=self.veda_id).latest()
self.video_proto = VideoProto(
veda_id=self.video_query.edx_id,
val_id=self.video_query.studio_id,
client_title=self.video_query.client_title,
duration=self.video_query.video_orig_duration,
bitrate='0',
s3_filename=self.video_query.studio_id
)
self.encode_query = Encode.objects.get(
product_spec=self.encode_profile
)
Video.objects.filter(
edx_id=self.video_query.edx_id
).update(
video_trans_status='Progress'
)
self.encoded_file = '/'.join((
self.video_query.edx_id,
self.video_query.edx_id + '.m3u8'
))
# HLS encodes are a pass through
self.hls_run()
else:
if os.path.exists(WORK_DIRECTORY):
......@@ -105,14 +76,6 @@ class VedaDelivery:
os.mkdir(WORK_DIRECTORY)
self._INFORM_INTAKE()
"""
Update Video Status
"""
Video.objects.filter(
edx_id=self.video_proto.veda_id
).update(
video_trans_status='Progress'
)
if self._VALIDATE() is False and \
self.encode_profile != 'youtube' and self.encode_profile != 'review':
......@@ -122,11 +85,8 @@ class VedaDelivery:
self._DETERMINE_ROUTE()
if self._VALIDATE_URL() is False and self.encode_profile != 'hls':
"""
Remember: youtube will return 'None'
"""
print 'ERROR: Invalid URL // Fail Out'
return None
# For youtube URLs (not able to validate right away)
return
"""
if present, set cloudfront distribution
......@@ -159,12 +119,6 @@ class VedaDelivery:
u1.md5_sum = self.video_proto.hash_sum
u1.save()
"""
Transcript, Xuetang
"""
self._XUETANG_ROUTE()
self.status = self._DETERMINE_STATUS()
self._UPDATE_DATA()
......@@ -183,6 +137,29 @@ class VedaDelivery:
if self.video_query.provider == TranscriptProvider.CIELO24:
self.cielo24_transcription_flow()
def hls_run(self):
"""
Get information about encode for URL validation/record
"""
self.video_query = Video.objects.filter(edx_id=self.veda_id).latest()
self.video_proto = VideoProto(
veda_id=self.video_query.edx_id,
val_id=self.video_query.studio_id,
client_title=self.video_query.client_title,
duration=self.video_query.video_orig_duration,
bitrate='0',
s3_filename=self.video_query.studio_id
)
self.encode_query = Encode.objects.get(
product_spec=self.encode_profile
)
self.encoded_file = '/'.join((
self.video_query.edx_id,
'{file_name}.{ext}'.format(file_name=self.video_query.edx_id, ext='m3u8')
))
def _INFORM_INTAKE(self):
"""
Collect all salient metadata and
......@@ -193,7 +170,6 @@ class VedaDelivery:
self.encode_query = Encode.objects.get(
product_spec=self.encode_profile
)
self.encoded_file = '%s_%s.%s' % (
self.veda_id,
self.encode_query.encode_suffix,
......@@ -206,13 +182,25 @@ class VedaDelivery:
self.auth_dict['veda_deliverable_bucket'],
self.encoded_file
))
os.system(
' '.join((
'wget -O',
os.path.join(self.node_work_directory, self.encoded_file),
self.hotstore_url
))
try:
conn = S3Connection()
bucket = conn.get_bucket(self.auth_dict['veda_deliverable_bucket'])
except NoAuthHandlerFound:
LOGGER.error('[VIDEO_DELIVER] BOTO/S3 Communication error')
return
except S3ResponseError:
LOGGER.error('[VIDEO_DELIVER] Invalid Storage Bucket')
return
source_key = bucket.get_key(self.encoded_file)
if source_key is None:
LOGGER.error('[VIDEO_DELIVER] S3 Intake Object NOT FOUND')
return
source_key.get_contents_to_filename(
os.path.join(self.node_work_directory, self.encoded_file)
)
"""
Utilize Metadata method in veda_utils -- can later
move this out into it's own utility method
......@@ -300,15 +288,16 @@ class VedaDelivery:
return None
if self.status == 'Complete':
val_status = 'file_complete'
self.val_status = 'file_complete'
else:
val_status = 'transcode_active'
print self.video_proto.val_id
self.val_status = 'transcode_active'
VAC = VALAPICall(
video_proto=self.video_proto,
val_status=val_status,
val_status=self.val_status,
endpoint_url=self.endpoint_url,
encode_profile=self.encode_profile
encode_profile=self.encode_profile,
CONFIG_DATA=self.auth_dict
)
VAC.call()
......@@ -611,49 +600,6 @@ class VedaDelivery:
self.video_query.studio_id,
)
def _XUETANG_ROUTE(self):
if self.video_query.inst_class.xuetang_proc is False:
return None
if self.video_query.inst_class.mobile_override is False:
if self.encode_profile != 'desktop_mp4':
return None
# TODO: un-hardcode
reformat_url = self.endpoint_url.replace(
'https://d2f1egay8yehza.cloudfront.net/',
'http://s3.amazonaws.com/edx-course-videos/'
)
prepared_url = veda_deliver_xuetang.prepare_create_or_update_video(
edx_url=reformat_url,
download_urls=[reformat_url],
md5sum=self.video_proto.hash_sum
)
w = Watchdog(10)
w.StartWatchdog()
try:
res = veda_deliver_xuetang._submit_prepared_request(
prepared_url
)
except (TypeError):
ErrorObject.print_error(
message='[ALERT] - Xuetang Send Failure'
)
return None
w.StopWatchdog()
if res.status_code == 200 and \
res.json()['status'] != 'failed':
URL.objects.filter(
encode_url=self.endpoint_url
).update(
xuetang_input=True
)
print str(res.status_code) + " : XUETANG STATUS CODE"
def YOUTUBE_SFTP(self, review=False):
if self.video_query.inst_class.yt_proc is False:
if self.video_query.inst_class.review_proc is False:
......
import os
import hashlib
import hmac
import base64
import datetime
import requests
import json
import time
from time import strftime
from VEDA.utils import get_config
"""
Authored by Ed Zarecor / edx DevOps
included by request
Some adaptations for VEDA:
-auth yaml
**VEDA Note: since this isn't a real CDN, and represents the
'least effort' response to getting video into china,
we shan't monitor for success**
"""
auth_dict = get_config()
API_SHARED_SECRET = auth_dict['xuetang_api_shared_secret']
API_ENDPOINT = auth_dict['xuetang_api_url']
# Currently the API support no query arguments so this component of the signature
# will always be an empty string.
API_QUERY_STRING = ""
SEPERATOR = '*' * 10
"""
This script provides a functions for accessing the Xuetang CDN API
It expects that an environment variable name XUETANG_SHARED_SECRET is
available and refers to a valid secret provided by the Xuetang CDN team.
Running this script will cause a video hosted in cloudfront to be
uploaded to the CDN via the API.
The status of the video will be monitored in a loop, exiting when
the terminal status, available, has been reached.
Finally, the video will be deleted from the cache exercising the
delete functionality.
"""
def _pretty_print_request(req):
"""
Convenience function for pretty printing requests for debugging API
issues.
"""
print('\n'.join(
[SEPERATOR + ' start-request ' + SEPERATOR,
req.method + ' ' + req.url,
'\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
req.body,
SEPERATOR + ' end-request ' + SEPERATOR]))
def build_message(verb, uri, query_string, date, payload_hash):
"""
Builds a message conforming to the Xuetang format for mutual authentication. The
format is defined in their CDN API specification document.
"""
return os.linesep.join([verb, uri, query_string, date, payload_hash])
def sign_message(message, secret):
"""
Returns a hexdigest of HMAC generated using sha256. The value is included in
the HTTP headers and used for mutual authentication via a shared secret.
"""
return hmac.new(bytes(secret), bytes(message), digestmod=hashlib.sha256).hexdigest()
def hex_digest(payload):
"""
returns the sha256 hexdigest of the request payload, typically JSON.
"""
return hashlib.sha256(bytes(payload)).hexdigest()
def get_api_date():
"""
Returns an "RFC8601" date as specified in the Xuetang API specification
"""
return strftime("%Y-%m-%dT%H:%M:%S") + "-{0:04d}".format((time.timezone / 3600) * 100)
def prepare_create_or_update_video(edx_url, download_urls, md5sum):
"""
Returns a prepared HTTP request for initially seeding or updating an edX video
in the Xuetang CDN.
"""
api_target = "/edxvideo"
payload = {'edx_url':edx_url, 'download_url': download_urls, 'md5sum':md5sum}
return _prepare_api_request("POST", api_target, payload)
def prepare_delete_video(edx_url):
"""
Returns a prepared HTTP request for deleting an edX video in the Xuetang CDN.
"""
api_target = "/edxvideo"
payload = {'edx_url':edx_url}
return _prepare_api_request("DELETE", api_target, payload)
def prepare_check_task_status(edx_url):
"""
Returns a prepared HTTP request for checking the status of an edX video
in the Xuetang CDN.
"""
api_target = "/edxtask"
payload = {'edx_url':edx_url}
return _prepare_api_request("POST", api_target, payload)
def _prepare_api_request(http_verb, api_target, payload):
"""
General convenience function for creating prepared HTTP requests that conform the
Xuetang API specificiation.
"""
payload_json = json.dumps(payload)
payload_sha256_hexdigest = hex_digest(payload_json)
date = get_api_date()
message = bytes(build_message(http_verb, api_target, API_QUERY_STRING, date, payload_sha256_hexdigest))
secret = bytes(API_SHARED_SECRET)
signature = sign_message(message, secret)
headers = {"Authentication": "edx {0}".format(signature), "Content-Type": "application/json", "Date": date}
req = requests.Request(http_verb, API_ENDPOINT + api_target, headers=headers, data=payload_json)
return req.prepare()
def _submit_prepared_request(prepared):
"""
General function for submitting prepared HTTP requests.
"""
# Suppress InsecurePlatform warning
requests.packages.urllib3.disable_warnings()
s = requests.Session()
# TODO: enable certificate verification after working through
# certificate issues with Xuetang
return s.send(prepared, timeout=20, verify=False)
if __name__ == '__main__':
# Download URL from the LMS
edx_url = "xxx"
# edx_url = "http://s3.amazonaws.com/edx-course-videos/ut-takemeds/UTXUT401T313-V000300_DTH.mp4"
# A list containing the same URL
download_urls = ["xxx"]
# The md5sum of the video from the s3 ETAG value
md5sum = "xxx"
#
# The code below is a simple test harness for the Xuetang API that
#
# - pushes a new video to the CDN
# - checks the status of the video in a loop until it is available
# - issues a delete request to remove the video from the CDN
#
# upload or update
# prepared = prepare_create_or_update_video(edx_url, download_urls, md5sum)
# _pretty_print_request(prepared)
# print os.linesep
# res = _submit_prepared_request(prepared)
# print res.text
# print os.linesep
# # check status
# while True:
# prepared = prepare_check_task_status(edx_url)
# _pretty_print_request(prepared)
# res = _submit_prepared_request(prepared)
# print res.text
# if res.json()['status'] == 'available':
# break
# time.sleep(5)
# delete file
prepared = prepare_delete_video(edx_url)
_pretty_print_request(prepared)
res = _submit_prepared_request(prepared)
print res.text
# check status
prepared = prepare_check_task_status(edx_url)
_pretty_print_request(prepared)
res = _submit_prepared_request(prepared)
print res.text
......@@ -38,7 +38,8 @@ class DeliverYoutube():
self.youtubekey = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'dependencies',
'youtube_callback',
'static_files',
'youtubekey'
)
......@@ -176,12 +177,14 @@ class DeliverYoutube():
if not os.path.exists(os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'dependencies',
'youtube_callback',
'static_files',
'delivery.complete'
)):
with open(os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'dependencies',
'youtube_callback',
'static_files',
'delivery.complete'
), 'w') as d1:
d1.write('')
......@@ -212,7 +215,8 @@ class DeliverYoutube():
s1.put(
os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'dependencies',
'youtube_callback',
'static_files',
'delivery.complete'
),
callback=printTotals,
......
......@@ -290,6 +290,7 @@ class Metadata():
return []
if video_object.video_active is False:
return []
"""
......@@ -301,6 +302,7 @@ class Metadata():
)
encode_list = E.determine_encodes()
if encode_list is not None:
if 'mobile_high' in encode_list:
encode_list.remove('mobile_high')
......@@ -362,17 +364,3 @@ class Metadata():
self.val_status = 'transcode_queue'
return encode_list
def main():
"""
Just to sneak a peek
"""
test_error = "This is a test"
ErrorObject.print_error(message=test_error)
E2 = EmailAlert(message='Test', subject='Test')
E2.email()
if __name__ == '__main__':
sys.exit(main())
......@@ -60,11 +60,10 @@ class VALAPICall():
self.headers = None
"""Credentials"""
self.auth_dict = self._AUTH()
self.auth_dict = kwargs.get('CONFIG_DATA', self._AUTH())
def call(self):
if self.auth_dict is None:
print 'No AUTH'
return None
"""
......@@ -91,7 +90,6 @@ class VALAPICall():
'username': self.auth_dict['val_username'],
'password': self.auth_dict['val_password'],
}
r = requests.post(self.auth_dict['val_token_url'], data=payload, timeout=self.auth_dict['global_timeout'])
if r.status_code != 200:
......
#!/usr/bin/python
"""
Simple Watchdog Timer
"""
'''
Stolen From:
--------------------------------------------------------------------------------
Module Name: watchdog.py
Author: Jon Peterson (PIJ)
Description: This module implements a simple watchdog timer for Python.
--------------------------------------------------------------------------------
Copyright (c) 2012, Jon Peterson
--------------------------------------------------------------------------------
'''
# Imports
from time import sleep
from threading import Timer
import thread
# Watchdog Class
class Watchdog(object):
def __init__(self, time=1.0):
''' Class constructor. The "time" argument has the units of seconds. '''
self._time = time
return
def StartWatchdog(self):
''' Starts the watchdog timer. '''
self._timer = Timer(self._time, self._WatchdogEvent)
self._timer.daemon = True
self._timer.start()
return
def PetWatchdog(self):
''' Reset watchdog timer. '''
self.StopWatchdog()
self.StartWatchdog()
return
def _WatchdogEvent(self):
'''
This internal method gets called when the timer triggers. A keyboard
interrupt is generated on the main thread. The watchdog timer is stopped
when a previous event is tripped.
'''
print 'Watchdog event...'
self.StopWatchdog()
thread.interrupt_main()
# thread.interrupt_main()
# thread.interrupt_main()
return
def StopWatchdog(self):
''' Stops the watchdog timer. '''
self._timer.cancel()
def main():
''' This function is used to unit test the watchdog module. '''
w = Watchdog(1.0)
w.StartWatchdog()
for i in range(0, 11):
print 'Testing %d...' % i
try:
if (i % 3) == 0:
sleep(1.5)
else:
sleep(0.5)
except:
print 'MAIN THREAD KNOWS ABOUT WATCHDOG'
w.PetWatchdog()
w.StopWatchdog() # Not strictly necessary
return
if __name__ == '__main__':
main()
......@@ -17,3 +17,4 @@ pysrt==1.1.1
MySQL-python==1.2.5
gunicorn==0.17.4
gevent==1.2.2
six==1.11.0
---
veda_s3_hotstore_bucket: s3_hotstore_bucket
veda_deliverable_bucket: s3_deliverable_bucket
edx_s3_endpoint_bucket: s3_deliverable_bucket
multi_upload_barrier: 2000000000
veda_base_url: https://veda.edx.org
s3_base_url: https://s3.amazonaws.com
edx_cloudfront_prefix: test_cloudfront_prefix
# transcript bucket config
aws_video_transcripts_bucket: bucket_name
......@@ -49,7 +52,6 @@ encode_dict:
- mobile_high
- mobile_low
- audio_mp3
- desktop_webm
- desktop_mp4
- hls
......@@ -61,24 +63,16 @@ encode_dict:
val_profile_dict:
mobile_low:
- mobile_low
desktop_mp4:
- desktop_mp4
override:
- desktop_mp4
- mobile_low
- mobile_high
mobile_high:
- mobile_high
audio_mp3:
- audio_mp3
desktop_webm:
- desktop_webm
youtube:
- youtube
review:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment