Commit 629d4ea6 by Иван Кубота

switch wp

parent ce36841c
*.png binary
*.jpg binary
*.mp4 binary
\ No newline at end of file
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
var Info = function(cfg, children) {
return D.div({cls: 'info'}, children)
};
var Content = function(cfg, children) {
return D.div({cls: 'content0'}, children)
};
var headers = [];
var Note = function(cfg, children) {
cfg = cfg || {};
return D.div({cls: D.cls('note', {'note-float': cfg.float})}, children)
};
var uniqLink = function(children) {
var child = children[0];
if(!child)
return {};
var title;
if(child.dom instanceof HTMLElement){
title = child.dom.innerText;
}else if(child instanceof HTMLElement){
title = child.innerText;
}else{
title = child;
}
return {name: title, title: title}
};
var Header = function(cfg, children) {
var item = uniqLink(children);
if(!cfg || !cfg.hidden)
headers.push(Object.assign(item, {type: 'Header'}));
return D.div({cls: 'header'}, [D.h('a', {attr: {name: item.name}})].concat(children))
};
var SubHeader = function(cfg, children) {
var item = uniqLink(children);
if(!cfg || !cfg.hidden)
headers.push(Object.assign(item, {type: 'SubHeader'}));
return D.div({cls: 'subHeader'}, [D.h('a', {attr: {name: item.name}})].concat(children))
};
var Field = function(cfg, children) {
cfg = cfg || {};
if(children[0].name === 'radio' || children[0].name === 'checkbox'){
return <div className={'field'}><label>{children} <span class='field-label'>{cfg.label}</span></label></div>
}else{
var child = children[0];
if(cfg.width && child){
if(child.dom instanceof HTMLElement){
child.dom.style.left = cfg.width+'px';
}else if(child instanceof HTMLElement){
child.style.left = cfg.width+'px';
}
}
return <div className={'field'}><label><span class='field-label'>{cfg.label}</span> {children}</label></div>
}
};
export {Info, Content, Note, Header, SubHeader, Field, headers};
\ No newline at end of file
import {Component} from 'cmp/Component.jsx';
import './Collapse.scss';
var Collapse = new Component({
name: 'Collapse',
ctor: function(cfg) {
Object.assign(this, cfg);
this.createDOM();
this.afterInit && this.afterInit();
},
prop: {
value: {type: Boolean, default: false},
label: {type: String, default: 'Collapse'},
},
createDOM: function() {
var val = this.value,
el = this.dom = this.inputEl = D.h('div', {
cls: D.cls('cmp-collapse', {
collapsed: val
})
},
D.div({cls: 'cmp-collapse-title', onclick: function(){ val.set(!val.get())}}, this.label),
D.div({cls: 'cmp-collapse-content'}, this.children)
);
this.afterDOM && this.afterDOM();
}
});
export { Collapse };
\ No newline at end of file
.cmp-collapse.collapsed .cmp-collapse-content {
display: none;
}
.cmp-collapse-title:before {
content: '▼';
margin: 0 4px 0 0;
}
.cmp-collapse.collapsed {
.cmp-collapse-title:before {
content: '▲';
}
}
.cmp-collapse-title {
cursor: pointer;
font-weight: bold;
margin: 32px 0 0 0;
}
.cmp-collapse-content {
padding-left: 16px;
}
\ No newline at end of file
...@@ -53,6 +53,9 @@ Component.prototype = { ...@@ -53,6 +53,9 @@ Component.prototype = {
} }
}, },
'~destroy': function() { '~destroy': function() {
// TODO: destroy props
var pointer = this.dom.parentNode; var pointer = this.dom.parentNode;
if(!pointer){ if(!pointer){
pointer = this.dom; pointer = this.dom;
......
import {Component} from 'cmp/Component.jsx';
var Input = new Component({
name: 'Input',
ctor: function(cfg) {
Object.assign(this, cfg);
this.createDOM();
this.initBinding();
this.afterInit && this.afterInit();
},
prop: {
value: {type: String, default: false}
},
createDOM: function() {
var val = this.value,
el = this.dom = this.inputEl = D.h('input', {
attr: {type: 'text'},
cls: 'cmp-input',
oninput: function() {
val.set(el.value);
}
});
this.afterDOM && this.afterDOM();
},
initBinding: function() {
var _self = this;
this.sub([this.value], function (val) {
_self.inputEl.value = val;
});
}
});
export { Input };
import {Component} from 'cmp/Component.jsx';
var NumberInput = new Component({
name: 'NumberInput',
ctor: function(cfg) {
Object.assign(this, cfg);
this.createDOM();
this.initBinding();
this.afterInit && this.afterInit();
},
prop: {
value: {type: String, default: false}
},
createDOM: function() {
var val = this.value,
el = this.dom = this.inputEl = D.h('input', {
attr: {type: 'number'},
cls: 'cmp-input',
oninput: function() {
val.set(el.value|0);
}
});
this.afterDOM && this.afterDOM();
},
initBinding: function() {
var _self = this;
this.sub([this.value], function (val) {
_self.inputEl.value = val;
});
}
});
export { NumberInput };
import {Component} from 'cmp/Component.jsx';
var TimeInput = new Component({
name: 'TimeInput',
ctor: function(cfg) {
Object.assign(this, cfg);
this.createDOM();
this.initBinding();
this.afterInit && this.afterInit();
},
prop: {
value: {type: Number, default: false},
min: {type: Number, default: 0},
max: {type: Number, default: 24*60*60},
},
createDOM: function() {
var _self = this;
this.dom = D.div({cls: 'TimeInput'},
this.minuteEl = D.h('input', {
attr: {type: 'number'},
cls: 'cmp-input minute',
oninput: function() {
_self.store.set('minutes', _self.minuteEl.value|0);
}
}), ':',
this.secondEl = D.h('input', {
attr: {type: 'number'},
cls: 'cmp-input second',
oninput: function() {
var val = _self.secondEl.value |0;
if(val<0){
_self.store.set({
'minutes': _self.store.get('minutes')-1,
'seconds': 59
});
}else if(val>59){
_self.store.set({
'minutes': _self.store.get('minutes')+1,
'seconds': 0
});
}else{
_self.store.set('seconds', val);
}
}
})
);
this.afterDOM && this.afterDOM();
},
initBinding: function() {
var _self = this;
this.sub([this.value], function (val) {
var sec = (val % 60) |0,
min = (val / 60) |0;
_self.store.set({
minutes: min,
seconds: sec
});
});
this.sub(['minutes', 'seconds'], function(min, sec) {
_self.value.set(min*60 + sec);
_self.minuteEl.value = min;
_self.secondEl.value = sec;
});
}
});
export { TimeInput };
...@@ -39,6 +39,15 @@ NS.apply = function(a,b) { ...@@ -39,6 +39,15 @@ NS.apply = function(a,b) {
el.removeAttribute(attrName) el.removeAttribute(attrName)
} }
} }
},
style: function(s, styleProp) {
return function(val) {
if(val !== void 0 && val !== false){
s[styleProp] = val;
}else{
delete s[styleProp];
}
}
} }
}; };
...@@ -114,7 +123,19 @@ NS.apply = function(a,b) { ...@@ -114,7 +123,19 @@ NS.apply = function(a,b) {
if(typeof style === 'string'){ if(typeof style === 'string'){
el.style = style; el.style = style;
}else{ }else{
NS.apply( el.style, style ); for( i in style ){
var s = el.style;
if(style.hasOwnProperty( i )){
if( typeof style[ i ] === 'function' ){
style[ i ]( setters.style( s, i ) );
}else if(typeof style[ i ] === 'object'&& style[ i ] !== null && style[ i ].hook){
return style[ i ].hook(setters.style(s, i));
}else{
setters.style( s, i )( style[ i ] );
}
}
}
//NS.apply( el.style, style );
} }
} }
...@@ -241,6 +262,24 @@ NS.apply = function(a,b) { ...@@ -241,6 +262,24 @@ NS.apply = function(a,b) {
isInDOM && D._recursiveCmpCall(el, {childNodes: newChild}, 'afterAddToDOM'); isInDOM && D._recursiveCmpCall(el, {childNodes: newChild}, 'afterAddToDOM');
}; };
D.insertAfter = function(newChild, refChild) {
var f = document.createDocumentFragment();
D.appendChild(f, newChild);
var el = refChild.parentNode,
subEl = newChild;
var isInDOM = D.isInDOM(el);
isInDOM && D._recursiveCmpCall(el, f, 'beforeAddToDOM');
var next = refChild.nextSibling;
if(next){
el.insertBefore( f, next );
}else{
el.appendChild( f );
}
isInDOM && D._recursiveCmpCall(el, {childNodes: newChild}, 'afterAddToDOM');
};
D.appendChild = function(el, subEl){ D.appendChild = function(el, subEl){
var type = typeof subEl; var type = typeof subEl;
...@@ -591,6 +630,18 @@ NS.apply = function(a,b) { ...@@ -591,6 +630,18 @@ NS.apply = function(a,b) {
}, true) }, true)
} }
}; };
D.findParent = function(el, fn) {
var test;
while(el){
test = fn(el);
if(test === true)
return el;
el = el.parentNode
}
};
D.s = D.h;
})(window['NS'], typeof window !== "undefined" ? window : })(window['NS'], typeof window !== "undefined" ? window :
typeof WorkerGlobalScope !== "undefined" ? self : typeof WorkerGlobalScope !== "undefined" ? self :
typeof global !== "undefined" ? global : typeof global !== "undefined" ? global :
......
...@@ -42,6 +42,9 @@ var Transform = (function() { ...@@ -42,6 +42,9 @@ var Transform = (function() {
var Transform = { var Transform = {
toFixed: hookAndConvert(function(val, digits) { toFixed: hookAndConvert(function(val, digits) {
return (val-0).toFixed(digits); return (val-0).toFixed(digits);
}),
concat: hookAndConvert(function(val, val2) {
return val + val2;
}) })
}; };
......
...@@ -277,7 +277,7 @@ Store.prototype = { ...@@ -277,7 +277,7 @@ Store.prototype = {
} }
!suppresFirstCall && wrap(); !suppresFirstCall && caller();
return function() { return function() {
for( var i = 0, _i = uns.length; i < _i; i++ ){ for( var i = 0, _i = uns.length; i < _i; i++ ){
uns[ i ](); uns[ i ]();
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
<script src="core/Observer.js"></script> <script src="core/Observer.js"></script>
<script src="core/DOM.js"></script> <script src="core/DOM.js"></script>
<script src="core/data/Transform.js"></script>
<script src="core/Localizer.js"></script> <script src="core/Localizer.js"></script>
<script src="core/Random.Seeded.js"></script> <script src="core/Random.Seeded.js"></script>
...@@ -22,17 +23,52 @@ ...@@ -22,17 +23,52 @@
$COMMIT$ $COMMIT$
$BUNDLE$ $BUNDLE$
<style> <style>
li {
margin: 32px 0;
}
a[target]:after{
content: '⭧';
}
a {
position: relative;
}
td { td {
padding: 4px 8px; padding: 4px 8px;
text-align: center; text-align: center;
} }
.content {
padding: 4px 32px;
margin: 32px -32px;
border-left: 2px solid;
width: 720px;
box-sizing: border-box;
}
.content0 {
padding: 4px 32px;
margin: 32px -32px;
width: 720px;
box-sizing: border-box;
}
i {
font-style: normal;
border-bottom: 2px solid #ff5e00;
}
.form {
padding: 4px 32px;
background: #eee;
margin: 32px -32px;
border-left: 2px solid;
width: 720px;
}
.info { .info {
background: #fffabc; background: #fff0c6;
padding: 16px 32px; padding: 16px 32px;
margin: 16px 0; margin: 16px 0;
} }
.page-content { .page-content {
max-width: 1024px; max-width: 920px;
margin: auto; margin: auto;
font-family: 'Literata', serif; font-family: 'Literata', serif;
position: relative; position: relative;
...@@ -46,12 +82,12 @@ ...@@ -46,12 +82,12 @@
.header { .header {
font-weight: bold; font-weight: bold;
font-size: 64px; font-size: 64px;
margin: 32px 0 16px; margin: 128px 0 32px;
} }
.subHeader { .subHeader {
font-weight: bold; font-weight: bold;
font-size: 32px; font-size: 32px;
margin: 32px 0 16px 0px; margin: 96px 0 48px 0;
} }
.thead td { .thead td {
border-bottom: 2px solid #000; border-bottom: 2px solid #000;
...@@ -62,8 +98,9 @@ ...@@ -62,8 +98,9 @@
font-size: 24px; font-size: 24px;
} }
a { a {
color: #1c7cf3; color: #1b9bcc;
font-family: sans-serif; font-family: 'Literata', serif;
margin: 0 4px;
} }
.note.note-float { .note.note-float {
float: right; float: right;
......
import '/model/Store'; import '/model/Store';
import {lal} from '/posts/post.jsx'; import {lal} from '/posts/post.jsx';
import {s} from '/posts/store.jsx';
import {VideoCalculator} from '/posts/VideoCalculator.jsx'; import {VideoCalculator} from '/posts/VideoCalculator.jsx';
import {Interactive} from '/posts/Interactive.jsx';
import { headers, Header, SubHeader } from "cmp/Blocks.jsx";
import { Collapse } from "cmp/Collapse/Collapse.jsx";
const {IF, ELSE} = Store; const {IF, ELSE} = Store;
export default function() { export default function() {
var krokotau = Store.Value.Boolean(false); var krokotau = Store.Value.Boolean(false);
let graph, let graph,
dom = <div renderTo={document.body} class={["page-content", {krokotau}]}> dom = <div renderTo={document.body} class={[ "page-content", { krokotau } ]}>
<button onClick={()=>krokotau.toggle()}>togl</button> {_ => {
Hello world! setTimeout( function(){
_( headers.map( i =>
<div class={D.cls( 'link-header', {
h1: i.type === 'Header',
h2: i.type === 'SubHeader'
} )}><a href={'#' + i.name}>{i.title}</a></div>
) )
}, 10 );
}}
<Collapse label={'Хранение и передача видео'} value={false}>
{VideoCalculator} {VideoCalculator}
</Collapse>
<Collapse label={'Интерактив'} value={false}>
<Interactive/>
</Collapse>
</div>; </div>;
const w = window; const w = window;
......
@import "/global-styles/variables.scss"; @import "/global-styles/variables.scss";
@import "/global-styles/mixins.scss"; @import "/global-styles/mixins.scss";
select, input {
font-size: 18px;
padding: 8px;
}
.field label {
select, input {
position: absolute;
left: 300px;
}
}
.field {
margin: 24px 0;
font-size: 18px;
}
\ No newline at end of file
import {Info, Content, Note, Header, SubHeader, Field} from '/cmp/Blocks.jsx';
import { PlainSelect } from "cmp/PlainSelect/PlainSelect.jsx";
import { Checkbox } from "cmp/Checkbox/Checkbox.jsx";
import { Radio } from "cmp/Radio/Radio.jsx";
import { Input } from "cmp/Input/Input.jsx";
import { TimeInput } from "cmp/TimeInput/TimeInput.jsx";
import { NumberInput } from "cmp/NumberInput/NumberInput.jsx";
import Add from "/svg/ic_add.svg";
import "./Interactive.scss";
import { Collapse } from "cmp/Collapse/Collapse.jsx";
var Interactive = function() {
s.set({
interactive: {
example1: {
selected: false,
name: 'Опрос 1',
from: 30,
to: 90,
now: 0,
title: 'Что это летает?!',
x: 20,
y: 15,
width: 60,
height: 60,
added: false,
options: [
'Рыба',
'Азатот'
],
'in': {
time: 1,
animation: 'style1'
},
'out': {
time: 1,
animation: 'style1'
}
}
}
});
var step = function(e) {
var stepEl = D.findParent(e.target, el=>el.classList.contains('step'));
if(stepEl){
var steps = [].slice.call(document.querySelectorAll('.step')),
stepID = steps.indexOf(stepEl);
var bound = steps[stepID+1].getBoundingClientRect();
steps[stepID+1].style.height = 0;
setTimeout(function() {
steps[stepID+1].style.height = bound.height + 'px';
}, 10);
//debugger
D.insertAfter(steps[stepID+1], stepEl);
}
};
var _now, vid,
dom = <div class={'interactive'}>
<Header>Интерактив</Header>
<Content>
<p>Возможность взаимодействия со зрителем отличает платформу от всех других продуктов доминирующих на рынке видеосервисов.</p>
<p>Мы предлагаем создателям контента получать моментальную обратную связь в виде голосований, разветвлений сюжета, конкурсов. На базе нашей платформы можно даже устроить розыгрыш призов или провести начальное тестирование для поступающих в вуз!</p>
</Content>
<SubHeader>Редактор интерактива</SubHeader>
<Content>
<p>
Интерфейс редактора позволяет добавлять интерактивные элементы и настраивать их. Рассмотрим на примере добавления опроса. Функционал не доступен в примере будет иметь серую подложку.
</p>
<p class='step'>
Запустим видео и найдём подходящий момент
<button class={'act'} onClick={e=>{vid.play();step(e);}}><img src={'play.png'}/></button>
</p>
</Content>
<div style='display: flex' class={D.cls('editor', {
notAdded: s.valEqual('interactive.example1.added', false)
})}>
<div style='display: flex;flex-direction: column;'>
<div style='position: relative'>
{vid = <video width="500" controls ontimeupdate={(e)=>{
s.set('interactive.example1.now', e.target.currentTime);
_now.style.left = e.target.currentTime/(3*60+20)*100+'%';
}}>
<source src="fish.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>}
<div class={D.cls('quiz', s.val('interactive.example1.style'))} style={{
left: T.concat( s.val('interactive.example1.x'), '%'),
top: T.concat( s.val('interactive.example1.y'), '%'),
height: T.concat( s.val('interactive.example1.width'), '%'),
width: T.concat( s.val('interactive.example1.height'), '%'),
opacity: _=>{
s.sub([
'interactive.example1.from',
'interactive.example1.in.time',
'interactive.example1.to',
'interactive.example1.out.time',
'interactive.example1.now'
], (f, ft, t, tt, n)=>{
if(n >= f && n <= t)
return _(1);
if(n<f-ft || n > t + tt)
return _(0);
if(n<f)
return _((f-n)/ft);
return _((t+tt-n)/tt);
})
}
}}>
<div class={'title'}>{s.val('interactive.example1.title')}</div>
<div className='answer'>{s.val('interactive.example1.options.0')}</div>
<div className='answer'>{s.val('interactive.example1.options.1')}</div>
</div>
</div>
<div class={'bottom-panel'}>
<div style='display: flex'>
<div class="timelines-list" style='width:120px'>
<div class={'add-button'}><Add/> Добавить
<div class={'inner'}>
<div class={'add-submenu'}><button onClick={()=>{
s.set('interactive.example1.from', vid.currentTime|0);
s.set('interactive.example1.to', vid.currentTime + 30|0);
s.set('interactive.example1.added', true);
}}>Опрос</button></div>
<div class={'not-implemented'} style={{display: 'block'}}>
<div class={'add-submenu'}><button>Вопрос</button></div>
<div class={'add-submenu'}><button>Переход</button></div>
<div class={'add-submenu'}><button>Разветвление сюжета</button></div>
<div class={'add-submenu'}><button>Результат опроса</button></div>
<div class={'add-submenu'}><button>Виджет</button></div>
</div>
</div>
</div>
<div class={'item'}>{s.val('interactive.example1.name')}</div>
</div>
<div class={'timeline-panel'}>
<div class={'timeline-title'}>
Timeline
</div>
<div class="timelines-bounds-list">
{_now = <div class={'time-now'}></div>}
{_=>{
var d;
_(d = <div class='kolbasa'>опрос</div>);
s.sub(['interactive.example1.from', 'interactive.example1.to'], (f,t)=>{
d.style.left = f/(3*60+20)*100+'%';
d.style.width = (t-f)/(3*60+20)*100+'%';
})
}}
<div class={'from'}>0:00</div>
<div class={'to'}>3:20</div>
</div>
</div>
</div>
</div>
</div>
<div style='width:100%;position: relative;padding:4px 16px' class={'inputs'}>
<Field label='Название' width={140}><Input value={s.bind('interactive.example1.name')}/></Field>
<Field label={'Стиль'} width={140}>
<PlainSelect value={s.bind('interactive.example1.style')} values={`
style1: Стиль 1
>style2: Стиль 2
style3: Стиль 3
`}/>
</Field>
<Collapse label={'Контент'} value={false}>
<Field label='Заголовок' width={140}><Input value={s.bind('interactive.example1.title')}/></Field>
<Field label='Ответ 1' width={140}><Input value={s.bind('interactive.example1.options.0')}/></Field>
<Field label='Ответ 2' width={140}><Input value={s.bind('interactive.example1.options.1')}/></Field>
<div class="not-implemented">+ Добавить ответ</div>
</Collapse>
<Collapse label={'Время'} value={true}>
<Field label='Начало (сек)' width={140}><TimeInput value={s.bind('interactive.example1.from')}/></Field>
<Field label='Конец (сек)' width={140}><TimeInput value={s.bind('interactive.example1.to')}/></Field>
</Collapse>
<Collapse label={'Позиция на экране'} value={true}>
<Field label='Левый край %' width={140}><NumberInput value={s.bind('interactive.example1.x')}/></Field>
<Field label='Верххий край %' width={140}><NumberInput value={s.bind('interactive.example1.y')}/></Field>
<Field label='Ширина %' width={140}><NumberInput value={s.bind('interactive.example1.width')}/></Field>
<Field label='Высота %' width={140}><NumberInput value={s.bind('interactive.example1.height')}/></Field>
</Collapse>
<Collapse label={'Анимации'} value={true}>
<p>Эффект появления</p>
<div className={'not-implemented'} style={{ width: '100%' }}>
<Field label={'Стиль'} width={180}>
<PlainSelect value={s.bind('interactive.example1.in.animation')} values={`
>style1: Проявление
style2: Вылет справа
style3: Выпрыгивание
`}/>
</Field>
</div>
<Field label='Время анимации' width={180}><TimeInput value={s.bind('interactive.example1.in.time')}/></Field>
<p>Эффект исчезновения</p>
<div className={'not-implemented'} style={{ width: '100%' }}>
<Field label={'Стиль'} width={180}>
<PlainSelect value={s.bind('interactive.example1.out.animation')} values={`
>style1: Растворение
style2: Улёт налево
style3: Ускакивание
`}/>
</Field>
</div>
<Field label='Время анимации' width={180}><TimeInput value={s.bind('interactive.example1.out.time')}/></Field>
</Collapse>
</div>
</div>
<Content>
<p class='step'>
Остановим видео в том месте где хочется добавить опрос
<button class={'act'} onClick={e=>{vid.pause();step(e);}}><img src={'pause.png'}/></button>
</p>
<p class='step'>
Для этого нужно навести на кнопку <img src={'add.png'}/> и выбрать «опрос» в появившемся списке.
</p>
<p class='step'>
Пользователь добавляющий опрос сможет настроить положение опроса на экране, время появления и анимации. Написать заголовок опроса и добавить любое количество ответов.
</p>
<p class='step'>
Многие параметры можно поменять и в этом интерактивном примере, но его цель — показать принцип.
</p>
</Content>
<Header>
Типы интерактива
</Header>
<SubHeader>
Опрос
</SubHeader>
<Content>
<p>
<i>Опрос</i>. Предлагает зрителю произвести <i>выбор ответа из предложенных вариантов</i>.
Выбрать можно как из приложения smartTV с помощью пульта, так и с мобильного телефона.
</p>
</Content>
</div>;
return dom;
};
export {Interactive};
\ No newline at end of file
.timeline-title {
margin: 8px 8px;
}
.editor, .editor .field, .editor select, .editor input {
font-family: Tahoma;
font-size: 12px;
}
.timelines-list {
.add-button {
display: flex;
height: 32px;
align-items: center;
svg {
margin-right: 8px;
}
}
.item {
background: #67919e;
border-bottom: 1px solid #67919e;
border-top: 1px solid #67919e;
border-left: 4px solid #fff;
border-right: 4px solid #fff;
color: #fff;
padding: 4px;
}
}
.editor {
.inputs {
transition: all 0.4s ease;
label {
display: flex;
align-items: center;
}
input, select {
border: 0;
border-bottom: 1px solid;
}
}
.field label {
select, input {
position: absolute;
left: auto;
}
}
.field label {
.TimeInput {
position: absolute;
select, input {
position: relative;
left: auto;
}
}
}
}
.TimeInput input {
width: 3em;
}
.time-now {
position: absolute;
z-index: 1;
border-left: 1px dotted #67919e;
left: 0;
top: -8px;
bottom: -8px;
}
.kolbasa {
background: #67919e;
border: 1px solid #67919e;
position: absolute;
color: #fff;
text-align: center;
padding: 4px 0;
top: 1px;
}
.bottom-panel {
padding: 8px 0;
margin-bottom: 32px;
height: 150px;
}
.timeline-panel {
width: 100%
}
.timelines-bounds-list {
border-left: 1px solid #67919e;
border-right: 1px solid #67919e;
margin: 0 4px;
padding: 8px 4px;
box-sizing: border-box;
position: relative;
height: 150px;
.from {
position: absolute;
bottom: -20px;
left: 0;
transform: translateX(-50%);
}
.to {
position: absolute;
bottom: -20px;
right: 0;
transform: translateX(50%);
}
}
.quiz {
transition: all 0.3s ease;
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background: rgba(255,255,255,0.5);
padding:8px 16px;
.title {
font-size: 28px;
}
.answer {
margin: 16px 0;
font-size: 18px;
&:before {
content: '⚪';
margin-right:8px;
}
}
}
.quiz.style1 {
background: rgba(86, 208, 255, 0.1);
.answer {
text-shadow: #19de76 1px -2px 5px;;
font-size: 22px;
font-family: CURSIVE;
color: #ffffff;
&:before {
font-size: 14px;
}
}
.title {
font-family: CURSIVE;
color: #ffffff;
text-shadow: #19de76 1px -2px 5px;;
}
}
.quiz.style2 {
transform: perspective(400px) rotateX(-15deg);
background: transparent;
color: #fff;
padding: 0;
.title {
font-size: 28px;
background: rgba(38, 87, 130, 0.7);
padding: 8px 16px;
margin-left: -16px;
text-align: left;
border-radius: 9px;
color: #ccecec;
}
.answer {
margin: 16px 0;
font-size: 18px;
background: linear-gradient(8deg, #8db4d0, transparent);
display: inherit;
padding: 8px;
border-radius: 25px;
border: 4px dotted #fff
}
}
.quiz.style3 {
border-radius: 20px;
transform: perspective(400px) rotateY(-5deg);
background: rgba(0,0,0,0.5);
color: #fff;
.title {
font-size: 28px;
text-align: center;
}
}
.not-implemented {
border-radius: 4px;
background: #ccc;
color: #777;
display: inline-block;
padding: 8px 16px;
cursor: not-allowed;
}
.act {
margin: 0 8px;
top: 12px;
vertical-align: middle;
}
.step {
overflow: hidden;
transition: all 0.4s ease;
}
.add-button {
cursor: pointer;
position: relative;
.inner {
display: none;
}
&:hover .inner{
display: block;
position: absolute;
left: 0;
top: 0;
width: 150px;
z-index: 2;
.add-submenu {
width: 100%;
button {
width: 100%;
cursor: pointer;
}
}
.not-implemented {
.add-submenu button {
background: #bbb;
}
}
}
}
.editor.notAdded {
.timelines-list .item {
display: none;
}
.kolbasa {
display: none;
}
.quiz {
display: none;
}
.inputs {
opacity: 0;
}
}
\ No newline at end of file
...@@ -2,28 +2,37 @@ import { DropdownField } from "cmp/dropdownField/DropdownField.jsx"; ...@@ -2,28 +2,37 @@ import { DropdownField } from "cmp/dropdownField/DropdownField.jsx";
import { PlainSelect } from "cmp/PlainSelect/PlainSelect.jsx"; import { PlainSelect } from "cmp/PlainSelect/PlainSelect.jsx";
import { Checkbox } from "cmp/Checkbox/Checkbox.jsx"; import { Checkbox } from "cmp/Checkbox/Checkbox.jsx";
import { Radio } from "cmp/Radio/Radio.jsx"; import { Radio } from "cmp/Radio/Radio.jsx";
import {Info, Content, Note, Header, SubHeader, Field} from '/cmp/Blocks.jsx';
var s = window.s = new Store({ import './VideoCalculator.scss';
size: '',
codec: 1, var months = [
framerate: 0, 'January',
time: 0, 'February',
'March',
filesize: 0, 'April',
filesize2: 0, 'May',
filesize3: 0, 'June',
perDay: 0, 'Jule',
simult: 0, 'August',
popularity: 0, 'September',
creating: 0, 'October',
'November',
useInterlacing: false, 'December'
calculation: 'tv-show', ];
backups: 0, var monthsShort = [
duration: 0, 'Jan',
dc: 0 'Feb',
}); 'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sept',
'Oct',
'Nov',
'Dec'
];
var { var {
size, size,
codec, codec,
...@@ -41,9 +50,11 @@ var { ...@@ -41,9 +50,11 @@ var {
calculation, calculation,
backups, backups,
duration, duration,
dc dc,
viewersFrom,
viewersTo
} = s.bindings(); } = s.bindings();
var AWSPrice = function(gb) { var AWSStorePrice = function(gb) {
if(gb<1024*50) if(gb<1024*50)
return 0.023; return 0.023;
...@@ -53,6 +64,31 @@ var AWSPrice = function(gb) { ...@@ -53,6 +64,31 @@ var AWSPrice = function(gb) {
return 0.021; return 0.021;
}; };
var AWSTransferPrice = function(gb) {
var sum = 0, pack;
pack = Math.min(gb, 9.999*1024);
sum += pack*0.09;
gb -= pack;
if(gb<=0.0001){
return sum;
}
pack = Math.min(gb, 40*1024);
sum += pack*0.085;
gb -= pack;
if(gb<=0.0001){
return sum;
}
pack = Math.min(gb, 100*1024);
sum += pack*0.07;
gb -= pack;
if(gb<=0.0001){
return sum;
}
sum += gb*0.05;
return sum;
};
var moneyFormatter = function(num) { var moneyFormatter = function(num) {
var strNum = num+''; var strNum = num+'';
var i, var i,
...@@ -80,55 +116,60 @@ var GBFormatter = function(gb) { ...@@ -80,55 +116,60 @@ var GBFormatter = function(gb) {
//if(gb-0 < 1024){ //if(gb-0 < 1024){
return (gb-0).toFixed(2)+'PB'; return (gb-0).toFixed(2)+'PB';
//} //}
}
var sizeSelector;
var Info = function(cfg, children) {
return D.div({cls: 'info'}, children)
};
var Note = function(cfg, children) {
cfg = cfg || {};
return D.div({cls: D.cls('note', {'note-float': cfg.float})}, children)
}; };
var Header = function(cfg, children) { var sizeSelector;
return D.div({cls: 'header'}, children)
};
var SubHeader = function(cfg, children) {
return D.div({cls: 'subHeader'}, children)
};
var Field = function(cfg, children) {
if(children[0].name === 'radio' || children[0].name === 'checkbox'){
return <div className={'field'}><label>{children} <span class='field-label'>{cfg.label}</span></label></div>
}else{
return <div className={'field'}><label><span class='field-label'>{cfg.label}</span> {children}</label></div>
}
};
var VideoCalculator = <div class={'calculator'}>
Видеосервис.
<Info> var VideoCalculator = <div class={'calculator'}>
<Header>
Видеосервис
</Header>
<Content>
<p> <p>
Видеосервис должен позволять загружать видео в популярных форматах. Хранение и передача видео в реальном времени — сложная и ресурсоёмкая задача с точки зрения инфраструктуры.
</p> </p>
<p> <p>
После загрузки видеосервер должен автоматически запускать процесс перекодирования видеопотока в форматы подходящий для передачи через интернет в браузеры и smartTV. Рассмотрим процесс от загрузки видеофайла создателем контента до проигрывания у конечного пользователя:
</p><p>
<ol>
<li><i>Видео</i> снято, смонтировано и <i>готово к загрузке</i> в нашу систему.</li>
<li>Выбираем видео в списке файлов и <i>начинаем загрузку</i>. Это значит что наша <i>система имеет достаточно места</i> для хранения данных. YouTube позволяет загружать файлы до 128GB.</li>
<li><i>Дисконнект</i>. До окончания загрузки осталось 6 часов, 3 часа уже прошло. YouTube в таком случае позволяет продолжить загрузку файла с контрольной точки. Это не стандартное поведение браузера и файлсервера. Такое решение требует отдельной разработки. Многие сервисы в таком случае предлагают начать процесс загрузки с начала.</li>
<li><i>Перекодирование</i>. Загрузка завершена! Теперь будем перекодировать файл в подходящие для браузеров и smartTV форматы. Перекодирование — это длительный и ресурсоёмкий процесс. Этот процесс хорошо распараллеливается, а значит его можно ускорить за счёт увеличения количества процессоров и ядер на серверах занимающихся перекодированием. Сейчас, в 2020 году, процесс перекодирования FullHD в 720p <a href="https://www.guru3d.com/articles-pages/intel-core-i9-9900k-processor-review,14.html" target="_blank">всё ещё занимает</a> время равное длительности загружаемое видео. Система должна одновременно перекодировать множество видео в большое количество форматов.
</li>
<li>
<p>
<i>Очередь задач</i> и <i>масштабирование</i>. Для общения частей системы выполняющих разные роли построим единую шину данных в которую будут добавляться новые задачи. Сервисы способные выполнить задачу будут забирать её из очереди и заниматься ей. Если очередь растёт быстрее чем задачи успевают обрабатываться — мы можем <i>моментально масштабировать инфраструктуру</i> путём добавления вычислительных узлов аналогичных загруженным.
</p><p>
Перекодирование в 144p занимает на порядки меньше времени чем перекодирование в 1080p и если издателю важно выложить контент как можно скорее — он может быть удовлетворён и таким стартом.
</p> </p>
</li>
<li>
<p> <p>
Обязательно должно производиться кодирование в видеофайлы с пониженным битрейтом для передачи пользователям не имеющим широкого канала интернет соединяния или оплачивающим траффик. <i>Публикация</i> и <i>доставка</i> на устройство пользователя. Видео выложено в публичный доступ. С этого момента мы <i>отслеживаем геолокацию зрителей</i>.
</p> </p>
</Info> <p>
Для экономии средств на передаче траффика и ускорения загрузки видео мы должны осуществлять доставку с серверов находящихся как можно ближе к зрителям. Можно построить свой <i>Content Delivery Network</i>, но оптимально будет воспользоваться готовыми. Как правило они дают всё необходимое для загрузки файлов и <i>балансировки нагрузки</i>.
</p>
</li>
</ol>
</p>
<p>
Каждая из описаных выше систем (перекодировка, дозагрузка, очередь задач, CDN, балансировщик) способна занять годы разработки, отладки и внедрения и не гарантирует что на выходе получится работоспособный результат. По этой причине мы настоятельно рекомендуем собрать прототип из наиболее стабильных общедоступных решений. После начального роста можно будет рассмотреть процесс замены дорогих сервисов на своими.
</p>
</Content>
<Note> <Note>
Интерактивные калькуляторы позволяют донести информацию наглядным образом и обеспечивают прозрачность Интерактивный калькулятор покажет из каких величин складывается стоимость хранения и передачи видеоконтента. Все поля ввода можно изменять. Результаты расчётов обновляются в реальном времени.
</Note> </Note>
<Header> <Header>
Хранение и передача видео Хранение и передача видео
</Header> </Header>
<SubHeader> <SubHeader>
Калькулятор размера видеофайла Размера видеофайла
</SubHeader> </SubHeader>
<div class={'field'}><label>Разрешение видео: {sizeSelector = <PlainSelect value={size} map={(val)=>({w: val.split('x')[0]-0, h: val.split('x')[1]-0})} values={` <div class={'field'}><label>Разрешение видео {sizeSelector = <PlainSelect value={size} map={(val)=>({w: val.split('x')[0]-0, h: val.split('x')[1]-0})} values={`
256x144 256x144
426x240 426x240
640x360 640x360
...@@ -144,13 +185,13 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -144,13 +185,13 @@ var VideoCalculator = <div class={'calculator'}>
s.set( 'use-' + item.id.split( 'x' )[ 1 ], true ); s.set( 'use-' + item.id.split( 'x' )[ 1 ], true );
}) })
}} }}
<div class={'field'}><label>Используемый кодек: <PlainSelect value={codec} map={Number} values={` <div class={'field'}><label>Используемый кодек <PlainSelect value={codec} map={Number} values={`
180: H.264 High quality 180: H.264 High quality
>340: H.264 Medium quality >340: H.264 Medium quality
675: H.264 Low quality 675: H.264 Low quality
`}/></label></div> `}/></label></div>
<div className={'field'}><label>Кадров в секунду: <PlainSelect value={framerate} map={Number} values={` <div className={'field'}><label>Кадров в секунду <PlainSelect value={framerate} map={Number} values={`
24 24
30 30
>60 >60
...@@ -159,12 +200,13 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -159,12 +200,13 @@ var VideoCalculator = <div class={'calculator'}>
`}/></label></div> `}/></label></div>
<div className={'field'}><label>Продолжительность серии: <PlainSelect value={time} map={Number} values={` <div className={'field'}><label>Продолжительность серии <PlainSelect value={time} map={Number} values={`
15: 15 минут 15: 15 минут
>30: 30 минут >30: 30 минут
60: 1 час 60: 1 час
120: 2 часа 120: 2 часа
135: 2 часа 15 минут 135: 2 часа 15 минут
210: 3 часа 30 минут
`}/></label></div> `}/></label></div>
...@@ -174,15 +216,15 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -174,15 +216,15 @@ var VideoCalculator = <div class={'calculator'}>
})} })}
<div className={'field'}>Размер видеофайла: {GBFormatter(filesize)}</div> <div className={'field'}>Размер видеофайла: {_=>s.sub([filesize],(filesize)=>_(GBFormatter(filesize)))}</div>
<SubHeader> <SubHeader>
Интерлейсинг Интерлейсинг
</SubHeader> </SubHeader>
<Info> <Content>
Технология широко использовалась во времена дисплеев основанных на лучевых трубках — чередовались кадры с чётными и нечётными строками. Позволяет получить двойной выигрыш по объёму файлов за счёт ухудшения качества картинки. Технология интерлейсинга или <i>чересстрочная развёртка</i> экономит половину места при хранении и передаче видеофайлов. Вместо полного кадра, пеередаются чётные и нечётные строки поочерёдно. Технологии распространилась из-за физических ограничений накладываемых принципами работы Электронно-Лучевой трубки. Мы тоже можем извлечь выгоду из этого метода, но не рекомендуем.
</Info> </Content>
<img src="https://blog.video.ibm.com/wp-content/uploads/2016/03/interlace-soccer2-1024x394.jpg" style="display: block;margin: auto;"/> <img src="https://blog.video.ibm.com/wp-content/uploads/2016/03/interlace-soccer2-1024x394.jpg" style="display: block;margin: auto;"/>
...@@ -212,8 +254,9 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -212,8 +254,9 @@ var VideoCalculator = <div class={'calculator'}>
s.set('filesize3', (downscales.map(item=>s.get('use-'+item.p)?item.size-0:0).reduce((a,b)=>a+b) * (useInterlacing ? 0.5 : 1)).toFixed(2)); s.set('filesize3', (downscales.map(item=>s.get('use-'+item.p)?item.size-0:0).reduce((a,b)=>a+b) * (useInterlacing ? 0.5 : 1)).toFixed(2));
_(<div><SubHeader>Объём необходимый для хранения различных форматов</SubHeader> _(<div><SubHeader hidden={true}>Объём занимаемый различными форматами</SubHeader>
<Note float>форматы которые не нравятся можно выключить</Note> <Content>Для хранения всех разрешений видео требуется в среднем в два раза больше места чем занимает видео в самом высоком разрешении. Форматы которые не нравятся можно выключить.</Content>
<table> <table>
<tr class={'thead'}> <tr class={'thead'}>
<td></td> <td></td>
...@@ -238,22 +281,25 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -238,22 +281,25 @@ var VideoCalculator = <div class={'calculator'}>
console.log(s.get('filesize3')); console.log(s.get('filesize3'));
}else{ }else{
s.set('filesize3', (downscales[0].size * (useInterlacing ? 0.5 : 1)).toFixed(2)); s.set('filesize3', (downscales[0].size * (useInterlacing ? 0.5 : 1)).toFixed(2));
_(<div>Объём необходимый для хранения одного выпуска: {GBFormatter(s.get('filesize3'))}</div>); _(<div>Объём необходимый для хранения одного выпуска {GBFormatter(s.get('filesize3'))}</div>);
} }
})} })}
<SubHeader>Выберите тип расчёта</SubHeader> <SubHeader>Выбор типа расчёта</SubHeader>
<Field label='Сериальный'><Radio value='tv-show' group={calculation}/></Field> <Content>
<Field label='Видеоплатформенный'><Radio value='ott' group={calculation}/></Field> <p>Видеоплатформы делятся на два типа — в одни контент загружают издатели (HBO, Netflix), а в другие — пользователи (YouTube, Vimeo, Pornhub). В нашем случае будет разработана смесь обоих подходов.</p>
</Content>
<Field label='Сериальный расчёт'><Radio value='tv-show' group={calculation}/></Field>
<Field label='Видеоплатформенный расчёт'><Radio value='ott' group={calculation}/></Field>
{_=>{ {_=>{
calculation.sub(val=>{ calculation.sub(val=>{
if(val === 'tv-show'){ if(val === 'tv-show'){
_(<div> _(<div>
<Header>Сериальный расчёт</Header> <Header hidden={true}>Сериальный расчёт</Header>
<SubHeader>Планируемая частота и количество шоу</SubHeader> <SubHeader hidden={true}>Количество и частота выходящих серий</SubHeader>
<Content>При сериальном расчёте занимаемый объём данных можно легко прогнозировать на полгода вперёд.</Content>
<div className={'field'}><label>Одновременно выходят: <PlainSelect value={simult} map={Number} values={` <div className={'field'}><label>Одновременно выходят <PlainSelect value={simult} map={Number} values={`
1: Одно шоу 1: Одно шоу
2: Два шоу 2: Два шоу
>5: Пять шоу >5: Пять шоу
...@@ -262,19 +308,19 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -262,19 +308,19 @@ var VideoCalculator = <div class={'calculator'}>
100: Шоу, челленджи, платформа популярна, но не на весь мир 100: Шоу, челленджи, платформа популярна, но не на весь мир
`}/></label></div> `}/></label></div>
<div class={'field'}><label>Новая серия выходит: <PlainSelect value={perDay} map={Number} values={` <div class={'field'}><label>Новая серия выходит <PlainSelect value={perDay} map={Number} values={`
1: Каждый день 1: Каждый день
>7: Каждую неделю >7: Каждую неделю
30: Каждый месяц 30: Каждый месяц
28: Сезон из 13 серий USA\Canada standard 28: Сезон из 13 серий USA\Canada standard
16.5: Сезон из 13 серий USA\Canada standard 16.5: Сезон из 22 серий USA\Canada standard
`}/></label></div> `}/></label></div>
</div>); </div>);
}else if(val === 'ott'){ }else if(val === 'ott'){
_(<div> _(<div>
<Header>Расчёт для видеоплатформы</Header> <Header hidden={true}>Расчёт для видеоплатформы</Header>
<Info>На основе <a href="https://merchdope.com/youtube-stats/#:~:text=The%20total%20number%20of%20people,on%20Youtube%20every%20single%20day.&text=In%20an%20average%20month%2C%208,49%20year%2Dolds%20watch%20YouTube" target='_blank'>статистики по ютубу за 2020 год</a> и <Info>На основе <a href="https://merchdope.com/youtube-stats/#:~:text=The%20total%20number%20of%20people,on%20Youtube%20every%20single%20day.&text=In%20an%20average%20month%2C%208,49%20year%2Dolds%20watch%20YouTube" target='_blank'>статистики по ютубу за 2020 год</a> и
доступных публичных данных <a href="https://www.similarweb.com/top-websites">от similarweb</a> были выстроены примерные оценки нагрузки на хранилища файлов для различных видеосервисов. доступных публичных данных <a href="https://www.similarweb.com/top-websites" target="_blank">от similarweb</a> были выстроены примерные оценки нагрузки на хранилища файлов для различных видеосервисов.
</Info> </Info>
<Field label={'Популярность'}> <Field label={'Популярность'}>
<PlainSelect value={popularity} map={Number} values={` <PlainSelect value={popularity} map={Number} values={`
...@@ -297,8 +343,9 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -297,8 +343,9 @@ var VideoCalculator = <div class={'calculator'}>
<SubHeader>Расчёт требуемого для хранения данных места</SubHeader> <SubHeader>Расчёт требуемого для хранения данных места</SubHeader>
<p> <p>
Расчитаем нарастающий объём данных на дисках. Для упрощения — будем считать что количество контента и зрителей нарастает линейно.
</p> </p>
<Field label={'Временной промежуток'}> <Field label={'Временной промежуток'}>
<PlainSelect value={duration} map={Number} values={` <PlainSelect value={duration} map={Number} values={`
1:1 month 1:1 month
...@@ -311,19 +358,10 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -311,19 +358,10 @@ var VideoCalculator = <div class={'calculator'}>
</Field> </Field>
<p> <p>
Будем считать что наполнение происходит линейно
</p>
<Field label={'Хранение бэкапов'}> </p>
<PlainSelect value={backups} map={Number} values={`
0: Мы не храним бэкапов!
1: RAID 10. Дублирование всех записанных данных
>0.3: RAID 3. Отдельный диск для битов чётности. Минимум 4 диска
2: Кворум из трёх машин. Обычно используется для финансовых БД
`}/>
</Field>
<Note> <Content>
<p> <p>
На первых этапах (демонстрация, первые 6 месяцев) можно раздавать контент с одного сервера. На первых этапах (демонстрация, первые 6 месяцев) можно раздавать контент с одного сервера.
</p> </p>
...@@ -331,17 +369,17 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -331,17 +369,17 @@ var VideoCalculator = <div class={'calculator'}>
Далее можно взять несколько серверов в оптимальных местах (европа, северная и южная америка, азия, западная и восточная Россия). Далее можно взять несколько серверов в оптимальных местах (европа, северная и южная америка, азия, западная и восточная Россия).
</p> </p>
<p> <p>
Но при серьёзном масштабировании OTT платформы нужно будет обязательно выбрать подходящее CDN решение или построить своё. При серьёзном масштабировании OTT платформы нужно будет обязательно выбрать подходящее CDN решение или построить своё.
</p> </p>
<p> <p>
CDN позволяет как экономить международный траффик между клиентом и сервером раздающим видео, так и снять нагрузку с ключевых элементов инфраструктуры. CDN позволяет как экономить международный траффик между клиентом и сервером раздающим видео, так и снять нагрузку с ключевых элементов инфраструктуры.
</p> </p>
<p> <p>
Много пользователей из Америки одновременно смотрящих премьеру передачи с европейского сервера способны существенно забить трансокеанические кабели передачи данных, или, как минимум, сделать его использование нерентабельным. Много пользователей из Америки одновременно смотрящих премьеру передачи с европейского сервера способны существенно забить трансокеанические кабели передачи данных, или, сделать всю затею нерентабельной.
</p> </p>
</Note> </Content>
<Field label={'Датацентры'}> <Field label={'Датацентры'} width={250}>
<PlainSelect value={dc} map={Number} values={` <PlainSelect value={dc} map={Number} values={`
0: Не нужен 0: Не нужен
>2: Разворачиваем 3 своих датацентров >2: Разворачиваем 3 своих датацентров
...@@ -349,28 +387,54 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -349,28 +387,54 @@ var VideoCalculator = <div class={'calculator'}>
`}/> `}/>
</Field> </Field>
<Note>
<Field label={'Хранение бэкапов'} width={250}>
<PlainSelect value={backups} map={Number} values={`
0: Мы не храним бэкапов!
1: RAID 10. Дублирование всех записанных данных
>0.3: RAID 3. Отдельный диск для битов чётности. Минимум 4 диска
2: Кворум из трёх машин. Обычно используется для финансовых БД
`}/>
</Field>
<Content>
<p> <p>
На больших объёмах основными потребителями ежемесячного бюджета становятся сервера и оплата траффика. Траффик напрямую зависит от количества пользователей платформы. На больших объёмах основными потребителями ежемесячного бюджета становятся сервера и оплата траффика. Траффик напрямую зависит от количества пользователей платформы.
</p> </p>
<p> <p>
Дальнейшие вычисления нагрузки будут произведены с учётом самого популярного разрешения видео 720p Пользователь Netfilx в среднем проводит 2 часа в день за просмотром контента Netflix. В дальнейших вычислениях потребляемого траффика будем считать что пользователь смотрит в день один час видео в самом популярном формате 720p.
</p> </p>
</Note> </Content>
<Field label={'Пользователей в месяц '}>
<PlainSelect value={dc} map={Number} values={` <Field label={'Пользователей в месяц в начале'} width={350}>
<PlainSelect value={viewersFrom} map={Number} values={`
0: Нет пользователей 0: Нет пользователей
20: 20 — Тестирование платформы >20: 20 — Тестирование платформы
500: 500 — Ранний запуск 500: 500 — Ранний запуск
4000: 4000 — Набирает популярность 4000: 4000 — Набирает популярность
10000: 10000 — Набирает популярность 10000: 10000 — Набирает популярность
180000000: 180 000 000 — Аудитория Netflix 180000000: 180 000 000 — Аудитория Netflix
`}/>
</Field>
<Field label={'Пользователей в месяц в конце'} width={350}>
<PlainSelect value={viewersTo} map={Number} values={`
0: Нет пользователей
20: 20 — Тестирование платформы
500: 500 — Ранний запуск 500: 500 — Ранний запуск
5: Разворачиваем 6 своих датацентров >4000: 4000 — Набирает популярность
10000: 10000 — Набирает популярность
180000000: 180 000 000 — Аудитория Netflix
`}/> `}/>
</Field> </Field>
<SubHeader>Результат для AWS</SubHeader>
<p>Данные в таблице отражают только объём и траффик потребляемый для хранения\передачи видео с разбивкой на месяцы. Внутреннее потребление на хранение оригиналов видеофайлов может оказаться даже выше расчётного если к проекту подключатся киностудии и начнут загружать оригинальный или Blu Ray формат</p>
<p>Цена стоимости траффика <a href="https://aws.amazon.com/mediastore/pricing/">вычисляется по данным с официального сайта AWS</a>.</p>
{_=>{ {_=>{
s.sub([ s.sub([
...@@ -382,7 +446,9 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -382,7 +446,9 @@ var VideoCalculator = <div class={'calculator'}>
simult, perDay, simult, perDay,
popularity, popularity,
time, time,
filesize3, dc filesize3, dc,
viewersFrom,
viewersTo, codec, framerate
], ( ], (
duration, duration,
...@@ -391,13 +457,19 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -391,13 +457,19 @@ var VideoCalculator = <div class={'calculator'}>
simult, perDay, simult, perDay,
popularity, time, popularity, time,
filesize3, dc)=>{ filesize3, dc,
viewersFrom,
viewersTo, codec, framerate
)=>{
var origDate = +new Date(), var origDate = +new Date(),
d = origDate, d = origDate,
lastDate = d, lastDate = d,
slices = [], slices = [],
stored = 0; stored = 0;
viewersTo|=0;
viewersFrom|=0;
var deltaViewers = (viewersTo-viewersFrom)/(duration-1);
for(var i = 0; i < duration; i++){ for(var i = 0; i < duration; i++){
d = new Date(d); d = new Date(d);
d.setMonth(d.getMonth()+1); d.setMonth(d.getMonth()+1);
...@@ -417,6 +489,10 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -417,6 +489,10 @@ var VideoCalculator = <div class={'calculator'}>
stored += newData; stored += newData;
slice.stored = stored; slice.stored = stored;
slice.newData = newData; slice.newData = newData;
slice.users = (viewersFrom + deltaViewers*i)|0;
slice.traffic = slice.users * (((+d) - (+lastDate))/24/1000)*(1280*720*3/codec*framerate)/1024/1024/1024;
lastDate = d; lastDate = d;
slices.push(slice); slices.push(slice);
} }
...@@ -426,13 +502,18 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -426,13 +502,18 @@ var VideoCalculator = <div class={'calculator'}>
<td>Дата</td> <td>Дата</td>
<td>Объём</td> <td>Объём</td>
<td>AWS Storage</td> <td>AWS Storage</td>
<td>AWS Data Transfer</td>
<td>Пользователей</td>
<td>Traffic Amount</td>
<td>AWS Traffic Price</td>
</tr> </tr>
{slices.map(slice=><tr> {slices.map(slice=><tr>
<td>{slice.date.getMonth() +' '+ slice.date.getFullYear()}</td> <td>{monthsShort[slice.date.getMonth()] +' '+ slice.date.getFullYear()}</td>
<td>{GBFormatter(slice.stored)}</td> <td>{GBFormatter(slice.stored)}</td>
<td>${moneyFormatter((slice.stored*AWSPrice(slice.stored))|0)}/month</td> <td>${moneyFormatter((slice.stored*AWSStorePrice(slice.stored))|0)}/month</td>
<td>${moneyFormatter((slice.stored*AWSPrice(slice.stored))|0)}/month</td> <td>{moneyFormatter(slice.users)}</td>
<td>{GBFormatter(slice.traffic)}/month</td>
<td>${moneyFormatter((AWSTransferPrice(slice.traffic))|0)}/month</td>
</tr>)} </tr>)}
</table>); </table>);
...@@ -441,25 +522,19 @@ var VideoCalculator = <div class={'calculator'}> ...@@ -441,25 +522,19 @@ var VideoCalculator = <div class={'calculator'}>
}); });
}} }}
2020 June | 100 Tb <Content>
2020 Jule | 200 Tb <p>Netflix использует для перекодирования и хранения видео Amazon AWS, а для раздачи — сразу несколько CDN провайдеров</p>
2020 August | 300 Tb <p><img src="amazon-structure.png" alt="amazon structure"/></p>
Таблица, график, стоимость у амазона
<a href="https://medium.com/refraction-tech-everything/how-netflix-works-the-hugely-simplified-complex-stuff-that-happens-every-time-you-hit-play-3a40c9be254b" target="_blank">
Netflix использует для хранения Amazon AWS
<Info>
<a href="https://medium.com/refraction-tech-everything/how-netflix-works-the-hugely-simplified-complex-stuff-that-happens-every-time-you-hit-play-3a40c9be254b">
Здесь можно почитать о том как строится инфраструктура подобная Netflix Здесь можно почитать о том как строится инфраструктура подобная Netflix
</a> </a>
</Info> </Content>
USE: https://www.wowza.com/products/streaming-engine/deployment-options USE: https://www.wowza.com/products/streaming-engine/deployment-options
<h1>TODO: Дополнить варианты реализации чтоб был выбор из трёх стульев</h1>
</div>; </div>;
......
select, input {
font-size: 18px;
padding: 8px;
}
.field label {
select, input {
position: absolute;
left: 300px;
}
}
.field {
margin: 24px 0;
font-size: 18px;
}
.h1 {
margin: 32px 0 0 0;
font-size: 24px;
}
\ No newline at end of file
var s = window.s = new Store({
size: '',
codec: 1,
framerate: 0,
time: 0,
filesize: 0,
filesize2: 0,
filesize3: 0,
perDay: 0,
simult: 0,
popularity: 0,
creating: 0,
useInterlacing: false,
calculation: 'tv-show',
backups: 0,
duration: 0,
dc: 0,
viewersFrom: 0,
viewersTo: 0
});
export {s};
\ No newline at end of file
Техническое задание на разработку видеосервиса и портала-аттракциона Техническое задание на разработку видеосервиса и портала-аттракциона
3. Портал
3.1 Структура
3.1.1 Проекты ( исторический, искусство, реалити, магазин на диване)
3.1.2 Выборки из проектов
3.1.2 Направления (Детектив, комедия, драма)
3.1.3 Творчество, его сортировка и возможность добавить своё
1. Видеосервис 1. Видеосервис
1.1 должен выдерживать нагрузку 1.1 должен выдерживать нагрузку
1.2 трэкает внимание пользователей 1.2 трэкает внимание пользователей
...@@ -13,12 +21,6 @@ ...@@ -13,12 +21,6 @@
2.3.1 Сканирование QR с экрана 2.3.1 Сканирование QR с экрана
2.3.2 Вход по двухфакторной авторизации 2.3.2 Вход по двухфакторной авторизации
3. Портал
3.1 Структура
3.1.1 Проекты ( исторический, искусство, реалити, магазин на диване)
3.1.2 Выборки из проектов
3.1.2 Направления (Детектив, комедия, драма)
3.1.3 Творчество, его сортировка и возможность добавить своё
3.2 Коллаборативность 3.2 Коллаборативность
3.2.0 Социальная сеть 3.2.0 Социальная сеть
......
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