Реклама — двигатель прогресса

от автора

"Реклама — двигатель прогресса" — эта легкая фраза, сказанная невзначай моей сестрой, описывает практически весь путь разработки простенького скрипта, который со временем вырос в небольшое клиент-серверное приложение. Итак, в данной статье я расскажу про: авторизацию на youtube с помощью perl, сложные приёмчики с ffmpeg, мимоходом пройдусь по json и sqlite, и покажу, чего стоят подборки видео на youtube.

С чего всё началось

Идея родилась достаточно просто. Просматривая как-то вечером на youtube очередную подборку прикольных видео, я поймал себя на раздражающей мысли, что не хочу смотреть рекламу, а еще — не хочу видеть одно и то же видео дважды. Эта мысль развилась в идею о том, что, вероятно, множество процессов создания подобных видео можно автоматизировать. Прикинув свои возможности, я понял, что мне вполне по силам накидать небольшой скрипт, который меня освободит от рекламы и баянов.

Disclaimer: я не программист, а инженер-микроэлектронщик, так что при оценке кода делайте скидку на этот момент.

Получение данных

У меня было на выбор два источника видео: coub.com и vine.co. Просмотрев контент с обоих сайтов, был сделан выбор в пользу coub.com, что было активно поддержано моей девушкой.

У coub.com относительно недавно появился API, который позволяет тягать с него много всяких данных. Надо сказать, что я не сразу подумал о возможности авторизации на этом сайте, ведь доступ к ендпойнтам открыт для всех желающих. А вот когда авторизовался, то понял, что делать этого не надо было: для авторизованных пользователей открывается куча контента NSFW(not safe for work, 18+), который, вообще говоря, не понятно что делает на этом сайте. Итак, работаем без авторизации.

Пример эндпойнта:

http://coub.com/api/v2/timeline/hot?page=${page_number}&per_page=${per_page}&order_by=newest_popular

Не буду приводить тут код функции, которая тягает с означенного эндпойнта JSON, так как они тривиальна и не интересна.

Работа с данными

Сначала я просто банально смотрел лидеров по количеству просмотров, потом пытался написать хитрые метрики для определения популярности видео, но все это не работало, как хотелось. Попробовал поиграть с кластеризацией, но тоже требуемого эффекта не получил.

В итоге я решил, что надо отслеживать динамику процесса, а для этого написал маленькую базу sqlite на две таблички, которая позволяет мне отслеживать просмотры по различным видео. Все манипуляции с базой лежат на плечах скрипта, который тягает JSON’ы с эндпойнтов, занимается разбором полученных данных и прочее. Также этот скрипт генерирует красивые картинки для понимания динамики процесса и создает JSON с конечными данными для последующего использования. Запускается скрипт раз в полчаса по cron’у.

На картинке хорошо виден набор просмотров днем и ночью, а также моменты публикации ссылок на видео на популярных порталах (ну или включения ботов накрутки просмотров, хе-хе). Время на графике — UTC, картинка кликабельна.

Для работы в perl со всем этим хозяйством мне потребовались следующие модули:

use LWP::Simple; use JSON::XS qw( decode_json );  use Time::Local; use DBI; use Chart::Gnuplot;

Надо отметить, что для работы с sqlite в дистрибутиве должен быть установлен DBD::Sqlite.

Формирование видео

Для формирования красивого видеоряда требуется некоторое время освоиться с одной замечательной утилитой — ffmpeg. Но когда вы научитесь ей пользоваться, возвращаться ко всяким avidemux’ам не захочется. Итак, какие полезные приемы я выучил за время написания скрипта? Начнем с простого.

Подготовка музыки

$local_batch = "$converter -t $audio_dur -i $music_source -af \"afade=t=out:st=$start_t_plus:d=$diff,afade=t=in:ss=0:d=$diff,volume=$volume_scale\" $res_dir/starter.mp3 -y"; system( $local_batch );

Данная команда отрезает от $music_source кусочек длиной $audio_dur с применением фильтров afade и volume, и сохраняет это в starter.mp3. Фильтр afade позволяет получить эффект повышения(fade-in) и понижения(fade-out) громкости, а volume изменяет громкость всей дорожки целиком.

Превращаем картинку в видео со звуком

$local_batch = "$converter -loop 1 -i $picture_source -i $res_dir/end.mp3 -c:v libx264 -t $end_t $res_dir/ending.mkv -y"; system( $local_batch );

Решаем проблему кривого разрешения

$local_batch  = "$converter -i ./video_source/source-video-$i.mp4 "; $local_batch .= "-filter_complex \""; $local_batch .= "[0]scale=iw*$scale:ih*$scale [sharp]; "; $local_batch .= "[0]scale=trunc(iw*$blur_scale/2)*2:trunc(ih*$blur_scale/2)*2,crop=$max_w:$max_h,boxblur=30 [blur]; "; $local_batch .= "[blur][sharp] overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2\" "; $local_batch .= "-q:v 0 -vb 20M ./video_source/source-video-$i.mpg -y"; system( $local_batch );

Вы много раз видели вертикальное видео с красиво размытым фоном, сделанным из этого же видео. Теперь вы знаете, как это сделать 🙂

Итак, что же за магия здесь происходит? На вход мы подаем наш ролик и включаем —filter_complex. Дальше мы берем это же видео и приводим к требуемому размеру с заранее рассчитанными коэффициентами и сохраняем его как [sharp]. Потом опять же входное видео приводим к размеру несколько больше требуемого, потом обрезаем его до требуемого размера и применяем размытие, сохраняем как [blur]. Финальный шаг — размещаем видео [sharp] поверх [blur] строго по центру — готово!

Зачем нужна возня с trunc? Дело в том, что ffmpeg не умеет отрезать от видео один пиксель, поэтому где-то вам придется привести размер видео к четному. Где вы это будете делать — на свое усмотрение.

Тёмная магия

Даже не столько магия, сколько способ сложно сделать простой эффект на видео. Требовалось сделать:

  1. Оверлейный полупрозрачный бокс с названием с fade-out в альфа канал. (Иначе говоря, плавно пропадающее вместе с боксом название)
  2. Fade-in, fade-out на видео дорожку, переход в белый цвет.

Я не ручаюсь, что этот способ оптимальный, но я нашел только этот.

my $opacity = '@0.4'; $local_batch  = "$converter -i ./video_source/video-$i.mpg -i ./audio_misc/cut-audio-$i.mp3 "; $local_batch .= " -vf \"drawbox=enable=\'between(t,0,$title_dur)\':y=(ih/1.3):color=black$opacity:width=iw:height=100:t=max, "; $local_batch .= " drawtext=enable=\'between(t,0,$title_dur)\':fontfile=$font:text=\'$title[$i]\':fontcolor=white:fontsize=50:x=(w-tw)/2:y=(h/1.3)+30, format=yuv444p \""; $local_batch .= " -codec:a copy -q:v 0 -vb 20M ./video_music/inter$i.mpg -y"; system( $local_batch );  $local_batch  = "$converter -i ./video_source/video-$i.mpg -i ./video_music/inter$i.mpg -filter_complex \""; $local_batch .= "[1]fade=out:st=$title_subt:d=$title_fade:alpha=1 [ovr]; "; $local_batch .= "[0][ovr] overlay=0:0:repeatlast=0, fade=in:st=0:d=$diff:color=white, fade=out:st=$video_duration_diff:d=$diff:color=white\" "; $local_batch .= "-codec:a copy -q:v 0 -vb 20M ./video_music/faded_inter$i.mpg -y"; system( $local_batch );

Разберем подробно, что здесь происходит. В первой части мы добавляем на видео в отведенные временные рамки полупрозрачный черный бокс, а поверх него — белый текст.

Во второй части, используя уже знакомый —filter_complex мы берем видео с боксом и надписью и используем на нем fade-out в альфа канал. Затем берем видео без бокса и надписи и накладываем поверх него [ovr], одновременно применяя к полученному результату fade-in, fade-out видео канала с переходом в белый цвет.

Склеивая вряд полученные таким образом видео, получается единый видеоряд с плавным переходом от одного ролика к другому через fade белого цвета.

Disclaimer: Мне потребовалось некоторое время, чтобы понять, что делать паузы между роликами на что-либо совершенно неуместно — это отнимает время, рассеивает концентрацию… а уж отсутствие fade-in/out по звуковому каналу это вообще насилие над ушами слушателя.

Окончание ролика

На youtube принято в конце ролика дать зрителю послушать какую-нибудь странную музыку и посмотреть под нее превью своих прошлых выпусков. Ок, сделаем это:

$local_batch  = "$converter -i $res_dir/ending.mkv -i $res_dir/OVRL1.mkv -i $res_dir/OVRL2.mkv -loop 1 -i $res_dir/sub.png "; $local_batch .= "-filter_complex \""; $local_batch .= "[1]scale=iw/$scale_factor:ih/$scale_factor,drawbox=0:0:iw:ih:color=white:t=5 [pip0]; "; $local_batch .= "[2]scale=iw/$scale_factor:ih/$scale_factor,drawbox=0:0:iw:ih:color=white:t=5 [pip1]; "; $local_batch .= "[3]scale=iw/$scale_factor:ih/$scale_factor [pip2]; "; $local_batch .= "[0][pip0] overlay=(main_w-2*overlay_w)/3:main_h/($scale_factor-1)-overlay_h-50:repeatlast=0 [pip_m]; "; $local_batch .= "[pip_m][pip1] overlay=2*(main_w-2*overlay_w)/3+overlay_w:main_h/($scale_factor-1)-overlay_h-50 [sum]; "; $local_batch .= "[sum][pip2] overlay=main_w/2-overlay_w/2:2*main_h/3:shortest=1\" "; $local_batch .= "-crf $quality -vb 20M $res_dir/ending.mp4 -y"; system( $local_batch );

Применяя всё тот же —filter_complex и превращение картинки в видео ряд, получаем финишную заставку. Не буду разбирать подробно, механизм работы всё тот же, просто несколько другое использование.

Работа с youtube

Возникает вопрос, что делать с полученным видео? Смотреть самому это, конечно, здорово, но можно и друзьям показать. Решено — запилим канал на ютубе.

Первые мысли были такие: ютуб — это гугл, значит, наверняка, есть библиотека под perl, а документация отменная. Вторые мысли: почему нет библиотеки под perl? Третьи: откуда ошибки в доках? Четвертые: чтоб я еще раз…

🙂

В общем пришлось самостоятельно разбираться как работать с ютубом из perl. Граблей я собрал немерянно, так как работать с web из perl’а мне еще не приходилось.

Авторизация на ютубе сделана через oauth2, что на пальцах выглядит так:

  1. Используя client_id, однократно получаем auth_token. Эта операция обязательно производится с участием человека.
  2. Используя auth_token, получаем access_token и refresh_token. При этом access — истекает за час, а refresh — постоянный, по нему мы обновляем access.
  3. Если access_token истек, обновляем его с использованием refresh_token.

Звучит просто, но есть нюансы. Не буду предлагать вам собирать все грабли повторно, просто предложу свой код.

Получаем auth_token

################################################################### ###  Одноразовый запрос на получение одобрения от пользователя  ### ###  Вместе с одобрением получаем auth_token                    ### ################################################################### $ua = LWP::UserAgent->new();   open( RESPONSE, ">", $response_file ); $req = POST 'https://accounts.google.com/o/oauth2/auth',   [     scope         => "https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.readonly https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube",     response_type => "code",     include_granted_scopes => "true",     access_type   => "offline",     redirect_uri  => "http://localhost/oauth2callback",     client_id     => "$client_id"   ]; $content = $ua->request($req)->as_string;  print RESPONSE $content; system("$browser $response_file");  print "Enter auth_token:\n"; my $the_code = <STDIN>;

Получаем access и refresh токены

################################################################### ###  Одноразовый запрос на получение access и refresh tokens    ### ################################################################### $req = POST 'https://accounts.google.com/o/oauth2/token',     [         code          => "$the_code", ### Это и есть auth_token с прошлого шага         client_secret => "$client_secret",         redirect_uri  => "http://localhost/oauth2callback",         client_id     => "$client_id",         grant_type    => "authorization_code",     ];     $json          = $ua->request( $req )->decoded_content;     $json_text     = decode_json( $json );     $refresh_token = $json_text->{'refresh_token'};     $access_token  = $json_text->{'access_token'};      print LOG $json;      close RESPONSE;

Обновление доступа

################################################################### ###  Многоразовый запрос на получение access token              ### ###  Получаем access по существующему refresh token             ### ################################################################### $req = POST 'https://accounts.google.com/o/oauth2/token',     [         client_id     => "$client_id",         client_secret => "$client_secret",         refresh_token => "$refresh_token",         grant_type    => "refresh_token"     ];  $content      = $ua->request($req)->as_string;  $content      =~  m/"access_token"\s+:\s+"(.*)",.*/; $access_token = $1;  print "Access token succesfully refreshed: $access_token\n";

Проверка доступа

################################################################### ###  Многоразовый запрос на проверку access token               ### ################################################################### if( $check_access == 1 ){     $req = POST 'https://www.googleapis.com/oauth2/v3/tokeninfo',         [             access_token   => "$access_token",         ];      $content = $ua->request($req)->decoded_content;      print "$content\n"; }

На этом приключения с ютубом не заканчиваются, так как мы пока только получили авторизацию, а нам надо еще и залить свое видео на канал. И тут появляется очередной нюанс, связанный с тем, что я писал скрипт под windows, а он в известной степени не совместим с linux, в то время как мне нужна была стабильная работа скрипта и там, и там.

Если вы не знали, то сообщаю: нельзя просто так взять и залить видео на ютуб (с).

Сперва нужно сделать запрос, в котором предоставить информацию о предстоящей загрузке, и только потом по известному линку можно будет заливать.

Получение загрузочного линка

$file_size    = -s $file; $headers = HTTP::Headers->new(     'Content-Type'              => 'application/json; charset=utf-8',     'Authorization'             => "Bearer $access_token",     'x-upload-content-type'     => 'video/mp4',     'X-Upload-Content-Length'   => $file_size );  $r = HTTP::Request->new( 'POST', $url, $headers ); $r->content( $message ); $response   = $ua->request( $r ); $upload_url = $response->header("Location");

В качестве сообщения мы отправляем корректно сформированный JSON. Тут важно обратить внимание на то, что в документации гугла бинарные опции JSON в некоторых примерах указываются как True/False, но внутренний парсер гугла воспринимает, та-дам!, бинарные опции как true/false. Одна большая буква из копипастного примера может стоить вам приличного количества нервов, ведь возвращаемая ошибка: Parser error.

Загрузка видео

$file_content = read_file( $file, binmode => ':raw', scalar_ref => 1 ); $headers = HTTP::Headers->new(     'Content_Length'            => "$file_size",     'Content-Type'              => 'video/mp4',     'Authorization'             => "Bearer $access_token" ); $r         = HTTP::Request->new('PUT', $upload_url, $headers, $file_content); $response  = $ua->request( $r ); $json      = $response->decoded_content; $json_text = decode_json( $json ); $resp_code = $response->status_line; $video_id  = $json_text->{'id'};

Здесь важна самая первая строчка. Конечно, сослаться на файл можно многими разными способами, но только так perl не влезает в файл и не пытается его открыть, одновременно модифицируя его. По сути, мы делаем ссылку на файл и указываем, как с ней работать: бинарно.

Загрузка превью

$url = "https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=$video_id"; $headers = HTTP::Headers->new(     'Content_Length'  => $thumbnail_size,     'Content-Type'    => 'image/jpeg',     'Authorization'   => "Bearer $access_token", ); $r          = HTTP::Request->new('POST', $url, $headers, $thumbnail_content); $response   = $ua->request($r); $upload_url = $response->header("Location"); $resp_code  = $response->status_line;  print LOG $response->decoded_content; print "Thumbnail upload init status: $resp_code\n";

Заключение

На данный момент я использую клиент-серверный подход для создания роликов. Скрипт, отвечающий за базу данных, крутится на VPS’ке от digitalocean, доступ к которой мне предоставил друг. Кодирование видео — весьма ресурсозатратная штука, поэтому эта задача оставлена на мой домашний ПК. Также из дома я могу по желанию проверить видео, которые пойдут в выпуск, поменять их количество, добавить зацикливание и так далее.

Смотреть контент с других развлекательных каналов я перестал, так как, очевидно, ручная работа других ютуберов значительно отстает в скорости от моего скрипта. А смотреть на древние баяны и рекламу у меня теперь причин нет.

От автора

  • Не бойтесь писать на perl’е — это просто.

  • Когда я только начал писать скрипт, было много головной боли, связанной с тем, что я привык к понятию переменной типа "регистр", и не сразу сообразил, что в perl’e надо использовать ссылки.

  • — Почему ты не пошел на "К", ведь ты классно программируешь?
    — Я не пошел на "К", именно потому, что люблю программировать.

    Разговор двух студентов с кафедры №27(микроэлектроника) МИФИ, К — факультет кибернетики.

ссылка на оригинал статьи https://habrahabr.ru/post/281182/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *