ReeePlayer – интервальное повторение фрагментов видео для изучения иностранных языков
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/getpro/habr/upload_files/908/037/0aa/9080370aa100d7c1a01aaf479af4178e.png)
Начать смотреть видео на иностранном языке не просто. Этим объясняется большое количество статей с самыми разными советами:
Смотреть с русскими субтитрами и не париться.
Смотреть с иностранными субтитрами и переводить на паузе, если не понятно.
Пересматривать многократно, комбинируя различные подходы.
По моему мнению, такие способы либо малоэффективные, либо выматывающие. Поэтому за несколько лет у меня выработался свой подход. Как и многие, при просмотре стараюсь понять на слух, контролируя себя английскими субтитрами. Но все непонятные места, содержащие в том числе незнакомые слова, сохраняю в виде фрагментов (клипов), которые тренирую впоследствии с помощью интервального повторения. Это помогает не только восприятию, но и заучиванию новых слов в контексте, причем вместе с их произношением. Побочным положительным эффектом является запоминание различных фактов, если смотреть образовательные видео.
Похожую концепцию использовал пользователь @kciray. Из его статьи я почерпнул много интересных идей, за что ему спасибо!
Разумеется, это не серебряная пуля. Самое главное – ваше упорство и регулярность занятий. Технические средства лишь в какой-то мере облегчают процесс изучения. Это же относится и к разработанному мной приложению ReeePlayer, которое изначально использовалось только для своих нужд, но, надеюсь, будет полезно и другим. Приложение позволяет:
Смотреть фильмы с двумя субтитрами.
Изменять скорость воспроизведения, не искажая тембр.
Быстро выделять и сохранять фрагменты видео вместе с текстом.
Обеспечивать возможность интервального повторения подобно тому, как оно организовано в карточках Anki.
Далее я расскажу о нескольких интересных проблемах, возникших при разработке. Ссылка на Гитхаб в конце статьи. Пока собрано только под Windows.
Интервальное повторение
Возможно вы видели такую картинку (хотя нет, не видели, я же ее сам нарисовал):
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/webt/i_/rx/zm/i_rxzmvjogoxdaacqeudoc9vi94.png)
Она говорит о том, что если повторять в определенное время через увеличивающиеся интервалы (например 1 час, 1 день, 2 дня, 3, 5, 8 дней и т. д.), то ваше знание будет поддерживаться на довольно высоком уровне. Есть куча исследований и алгоритмов, связанных с вычислением оптимального интервала следующего повторения в зависимости от того, насколько сложно было пользователю вспоминать на предыдущих повторениях. Впрочем, есть и много приверженцев простого подхода с фиксированными интервалами.
В первоначальной реализации я использовал экспоненциально увеличивающиеся интервалы с поправкой на указанную пользователем (то есть мной) сложность. Сам процесс повторения был довольно выматывающим – нужно постоянно оценивать насколько трудно было разобрать тот или иной клип или же вспомнить слова из него. Этот процесс быстро сводился к машинальному выбору средней сложности. В конце концов я оставил лишь одну кнопку для подтверждения просмотра.
Тем не менее, до текущей версии дожил механизм вычисления интервала следующего просмотра. Приведу пример. Допустим, вы, как положено, повторили карточку несколько раз и ее следующее повторение должно быть через 10 дней. А вы вернулись к ней лишь через полгода. Она наверняка будет забыта, и предлагать повторить ее через 20 дней было бы странно. По-хорошему нужно начинать сначала, как для новой карточки.
А теперь посмотрим на эту ситуацию с другой стороны, в которой вы прилежный пользователь. Прошло только 9 дней из 10, но у вас прямо сейчас есть свободная минутка. Почему бы не засчитать это повторение и назначить следующее, ладно пусть не через 20 дней, а через 18.
Так, на основе этих рассуждений, появилась концепция уровня клипа. С помощью уровня вычисляется интервал следующего повторения приблизительно так: interval = C * 1.618 ^ level
. Также от уровня зависит и функция изменения уровня в зависимости от прошедшего времени. Вот, например, для 5 уровня:
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/getpro/habr/upload_files/a4e/823/3c2/a4e8233c266eaf7fbd8ad099b17071f2.png)
Вы можете спросить, что за "почти +1 к уровню", почему нельзя увеличивать ровно на 1? Когда-то так оно и было, и на графике в районе 2.5 – 5 дней была горизонтальная прямая. Дело в том, что для меня терялся смысл раннего повторения, так как хотелось поддерживать целочисленный уровень, при котором создавалось ощущение, что все делаешь вовремя и правильно (привет, перфекционизм). Сейчас хорошо – как ни старайся, +1 все равно не получишь.
Выбор библиотек
Из всех занятий лучше всего у меня получается лежать на диване и писать приложения на C++ и Qt, поэтому над выбором языка думать не пришлось. Но вот с помощью чего показывать видео? Тут чуть сложнее. Так как приложение в первую очередь ориентировано на изучение языков, то очень хотелось реализовать поддержку изменения скорости воспроизведения без изменения тембра. Также неплохо было бы иметь возможность поставлять приложение сразу со всеми кодеками, не заставляя пользователя качать K-Lite или что-то подобное.
Самым простым и заманчивым было использовать QMediaPlayer, но он пролетает по обоим пунктам. Еще из очевидных решений – ffmpeg. Почитал Видеоплеер на базе ffmpeg и что-то приуныл. Но есть же QtAV! Эта штука прекрасно работает при нормальной скорости воспроизведения. Но стоило только изменить ее, как начинался дикий рассинхрон. Насколько я помню, аудио было адекватным, а видео превращалось в слайдшоу. Последнее, что оставалось попробовать – libVLC. Фактически эта библиотека является ядром популярного плеера VLC. От самого плеера, скорее всего, только формочки.
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/webt/gg/fn/hr/ggfnhrzglp-hzzz2qbki-ti2ihk.png)
Библиотека достаточно высокоуровневая, то есть можно несколькими строчками кода указать хэндл окна для видео, открыть файл, запустить воспроизведение:
libvlc_instance_t* inst = libvlc_new(0, NULL);
libvlc_media_t* m = libvlc_media_new_path(inst, "test.mp4");
libvlc_media_player_t* mp = libvlc_media_player_new_from_media(m);
libvlc_media_release(m);
auto handle = reinterpret_cast<unsigned __int64*>(widget->winId())
libvlc_media_player_set_hwnd(mp, handle);
libvlc_media_player_play(mp);
Более того существует библиотека vlc-qt, которую я использовал сначала, но затем отказался, так как код начал обрастать прямыми вызовами функций libVLC в обход vlc-qt, так как в последней они были не реализованы.
И на тот момент и сейчас существуют 2 версии: 3 и 4. Третья стабильная, четвертая не очень. Зато в ней реализована поддержка зацикленного воспроизведения указанного участка (A-B Loop). Пробую. Видео появляется, но маленькое, не растянутое на весь размер окна. Да не проблема, я и сам могу найти коэффициент масштабирования, вызвать libvlc_video_set_scale
и получить вылет программы за такую самодеятельность. Пишу на форум, прикладываю запрошенную трассировку стека, вдохновляюсь ответом: "Seems like a regression in the VLC Windows windowing code".
Последней стабильной версией на тот момент была 3.0.11. С ней все началось хорошо, автомасштабирование заработало из коробки. Однако в некоторых случаях попытка программно задать текущую позицию видео приводила к лагу в 2-3 секунды. Причина, судя по логам, очень странная: плеер рапортовал, что успешно спозиционировался, но затем передумывал и долго перематывал на 10 миллисекунд назад. К счастью, в 3.0.8 этой проблемы не было. До сих пор используется именно эта версия.
Запустить воспроизведение заданного отрезка видео очень просто. Сложнее остановить в точно указанное время. Событие изменения текущий позиции прилетает примерно раз в полсекунды, что крайне редко для точной остановки. Позиция, запрашиваемая вручную по таймеру, выдает те же самые значения. Я использовал такое решение: после получения события самостоятельно отсчитывать время по таймеру, например раз в 10 мс, до следующего события.
Способ хранения клипов
Вопрос хранения клипов был самым трудным. При разработке одной из ранних версий мне казалось отличной идеей запихнуть всю информацию в sqlite-файл. Технически это было неплохим решением, которое прекрасно ложилось в базу данных в виде таблиц:
# files <-- clips <-- texts
CREATE TABLE "files" (
"path" TEXT
);
CREATE TABLE "clips" (
"file_id" INTEGER,
"begin" INTEGER,
"end" INTEGER,
"time" INTEGER,
"rep_time" INTEGER
);
# Эта таблица позволяет осуществлять полнотекстовой поиск
CREATE VIRTUAL TABLE texts USING FTS5(clip_id, idx, text);
Как видите, пути задавались жестко. Хотя они были относительными, тем не менее это делало невозможным изменение файловой иерархии. Реальный пример: я создал специальный каталог, покидал в корень несколько файлов, посмотрел. Затем скачал еще несколько видео с другого канала той же тематики в отдельный каталог, чтобы не мусорить:
(root)
[Канал "Космонавтика Древней Греции"]
Проблема сброса античных статуй из космоса.mp4
Как незаметно прикрыть дыру в пространственно-временном континууме.mp4
Через какое-то время, когда появились видео из совсем разных областей, я захотел объединить первые в одном общем каталоге:
(root)
[Космос]
[Канал "Космонавтика Древней Греции"]
Проблема сброса античных статуй из космоса.mp4
[Канал "Орбитальные лайфхаки"]
Как незаметно прикрыть дыру в пространственно-временном континууме.mp4
Но сделать этого было нельзя без редактирования базы вручную. Для решения была реализована концепция библиотеки – что-то вроде иерархического плейлиста, что позволяло как угодно структурировать контент.
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/webt/au/d3/s5/aud3s5xpf1kwionpnjiwd_xet9g.png)
Но это было неудобно: сначала нужно ответственно определить в какую директорию положить видео (ведь изменить уже нельзя), а затем еще добавить в библиотеку.
Также меня тревожила необходимость объяснить простому пользователю, что не надо хранить базу и видеофайлы в сильно разных местах, так как относительные пути превратятся в чёрт-те что, и при переезде на другой компьютер они наверняка станут указывать не туда.
В конце концов я решился на то, что совсем не хотел делать изначально – мусорить прямо рядом с медиафайлами. Это некрасиво. Но оказалось очень удобным хранить все клипы каждого видео в отдельном файле:
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/webt/bh/oe/4h/bhoe4hx3ojteo6gdmdppfhiw158.png)
Выбранный формат JSON позволяет наглядно просмотреть содержимое:
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/webt/mt/ph/qh/mtphqhyhezunghuyl23laubuv6u.png)
Недостатки следующие:
Замусоривание.
Должны быть права на запись.
Теоретически открытие директории с большим количеством файлов займет некоторое время.
Для двух пользователей нужны разные файлы. Технически решаемо именами вида
*.sasha.json
,*.masha.json
и т. п. А пока просто*.user.json
.Усложняется синхронизация и бэкапы. С git особых проблем быть не должно, но в Dropbox или Яндекс-диск просто так не положишь. Как вариант – добавить в приложение возможность создания бэкапа.
Зато никаких файлов-проектов, можно просто открыть любую директорию с видео- или аудио-файлами, и все клипы внутри нее будут прочитаны. Можно также, например, смотреть на работе (не рекомендую), а потом копировать на домашний компьютер.
![](https://webcf.waybackmachine.org/web/20210616152114/https://habrastorage.org/getpro/habr/upload_files/d3b/340/8a4/d3b3408a4f3c5abaee023dd70b0fc500.png)