вторник, 26 февраля 2008 г.

Запоминание позиции воспроизведения файла в MPlayer

Думаю, многим знакома следующая ситуация. Вы запускаете свой любимый MPlayer, чтобы посмотреть какой-нибудь фильм, смотрите, но до конца досмотреть не успеваете, т. к. вам нужно куда-нибудь уходить. Вы закрываете MPlayer, выключаете компьютер и идете по своим делам. Вернувшись, вы решаете досмотреть фильм. Вот только где же вы остановились? Что делать? "Проматывать", пока не наткнетесь на тот момент, который не видели? Записывать время остановки на бумажке? :) Нет, это не наш метод...

Когда я в очередной раз столкнулся с этой проблемой, то пошел на домашнюю страницу MPlayer, на которой нашел два скрипта для ее решения: mplayer-resume и MPlayer Tools.

mplayer-resume у меня отказался запоминать позиции в файлах и к тому же подавлял весь вывод mplayer'a, что довольно неаккуратно с его стороны, так что я сразу же отказался от него, а MPlayer Tools показался мне слишком неудобным в использовании. Поэтому я решил изобрести собственный велосипед. :)

В итоге на свет появился относительно небольшой скрипт, представленный ниже. Скрипт полностью сохраняет вывод MPlayer'a и может принимать все аргументы, которые принимает MPlayer. В том числе ему можно передавать одновременно несколько файлов для воспроизведения - каждый из них он воспроизведет с того места, на котором было остановлено воспроизведение в прошлый раз.

Краткое описание можно прочитать в комментариях, располагающихся в начале самого скрипта.

Удачи, надеюсь, скрипт окажется вам полезен и сэкономит хотя бы немного вашего времени и нервов. :)

mplayer.ext:
#!/bin/bash
#***************************************************************************
#*   Copyright (C) 2008, Konishchev Dmitry                                 *
#*   http://konishchevdmitry.blogspot.com/                                 *
#*                                                                         *
#*   Project homepage:                                                     *
#*   http://sourceforge.net/projects/mplayerext/                           *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU General Public License as published by  *
#*   the Free Software Foundation; either version 3 of the License, or     *
#*   (at your option) any later version.                                   *
#*                                                                         *
#*   This program is distributed in the hope that it will be useful,       *
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#*   GNU General Public License for more details.                          *
#**************************************************************************/

# mplayer.ext - скрипт-оболочка для mplayer.
# 
# Предназначен для продолжения прослушивания/просмотра аудио и видео
# файлов с той позиции, на которой просмотр/прослушивание завершился в
# прошлый раз при закрытии mplayer'a.
# 
# Использование:
# Если вы хотите пользоваться возможностями скрипта, вам необходимо
# всегда, когда вы хотите проиграть аудио/видео файл, вызывать этот скрипт
# вместо mplayer'a.
# 
# Как работает скрипт:
# Если завершение работы mplayer'a происходит во время просмотра фильма,
# то в файл, путь к которому задан переменной $resume_info_file, заносится
# информация о времени, на котором произошло прерывание просмотра. Время
# привязывается к имени файла (имени, а не пути!), таким образом, если файл
# будет перемещен в другую дирректорию, то скрипт все равно его "узнает".
# В следующий раз, когда пользователь запросит проигрывание этого фильма,
# скрипт просмотрит файл, заданный переменной $resume_info_file и
# продолжит воспроизведение фильма с того момента, на котором завершилось
# воспроизведение в прошлый раз.
# 
# Максимальное количество файлов, информация о которых может храниться в
# $resume_info_file задается переменной $max_resume_info_length.
# 
# Ограничения скрипта:
# * Скрипт не обрабатывает файлы DVD вида VTS_*_*.VOB, т. к. mplayer не
#   позволяет начинать воспроизведение таких файлов с произвольного места.
# * Т. к. mplayer позволяет начинать воспроизведение фильма только с
#   ключевого кадра, то, если воспроизведение фильма в прошлый раз
#   прервалось не на ключевом кадре, при попытке воспроизвести фильм с
#   того же места произойдет перемотка вперед до следующего ключевого
#   кадра, т. е. часть фильма останется непросмотренной. В связи с этим
#   скрипт производит "отматывание" на $keyint секунд назад (по умолчанию
#   - 10), т. к., при кодировании большинства MPEG-4 файлов данная
#   величина используется в качестве максимального расстояния между
#   ключевыми кадрами. Если в ваших видеофайлах интервал между ключевыми
#   кадрами больше этого значения, то измените значение переменной $keyint.  


# Настройки -->
# Максимальный интервал между ключевыми кадрами
keyint=10

# Файл, в котором будет храниться информация о недосмотренных файлах
resume_info_file=~/.mplayer/resume_info

# Максимальное количесво файлов, информация о которых будет храниться в $resume_info_file
max_resume_info_length=100
# Настройки <--


mplayer_ext_echo()
{
echo "mplayer.ext: $@"
}


mplayer_ext_warning()
{
mplayer_ext_echo "$@" >&2
}


cleanup()
{
rm -f $tmp_file
}


die()
{
mplayer_ext_warning "$@"
cleanup
exit 1
}


# Возвращает идентификатор видео по имени файла
get_video_name_by_file_name()
{
local video_name=$(basename "$1")

if [[ "$video_name" == "" ]]
then
return 1
fi

# Не обрабатываем файлы DVD, т. к. в них невозможно осуществлять воспроизведение
# с произвольного места
if echo -n "$video_name" | egrep -i '^vts_[[:digit:]]+_[[:digit:]]+.vob$' > /dev/null
then
return 1
fi

echo -n "$video_name"
}


# Если $2 == 0, то файл помечается как просмотренный
set_resume_pos()
{
declare -a resume_info_array
local resume_info resume_info_time resume_info_time i

# Устанавливаем разделитель слов равным \n
local IFS=$'\n'

i=0
for resume_info in `cat "$resume_info_file" | tail --lines $((max_resume_info_length - 1))`
do
resume_info_time=`echo -n "$resume_info" | egrep '^.+:[[:digit:]]+$' | sed -r 's/^.+://' | egrep '^[[:digit:]]+$'`
resume_info_name=`echo -n "$resume_info" | sed "s/:${resume_info_time}\$//"`

# Пропускаем неверно сформированные записи
if [[ "$resume_info_time" == "" || "$resume_info_name" == "" ]]
then
mplayer_ext_warning "Bad resume info string: '$resume_info'."
continue
fi

# Если это тот файл, который мы ищем
if [[ "$resume_info_name" == "$1" ]]
then
# Пропускаем старую запись
continue
# Остальные файлы - оставляем без изменений
else
resume_info_array[$((i++))]="$resume_info"
fi
done

# Если видео не досмотрели до конца
if [[ "$2" != "0" ]]
then
mplayer_ext_echo "Writing resume time information: '$1': $2."
resume_info_array[$i]="$1:$2"
else
mplayer_ext_echo "Writing resume time information: '$1': viewed."
fi

# Заносим изменения в файл
echo "${resume_info_array[*]}" > "$resume_info_file" || die
}


# Получает строку времени, на котором было приостановлено воспроизведение файла.
# Преобразует строки вида:
# A: 308.4 V: 308.4 A-V:  -0.006 ct:  -0.041 7395/7395  4%  0%  5.5% 0 0
# A: 308.4 V: 308.4 A-V:  0.006 ct:  0.041 7395/7395  4%  0%  5.5% 0 0
# A:   2.0 V:   2.0 A-V: -0.006 ct:  0.007  50/ 50  6%  3%  3.9% 0
# A:  87.6 (01:27.5) of 228.0 (03:48.0)  4.4%
# V:   1.8  45/ 45 15%  3%  0.0% 0 0
# в строку вида:
# [AV]:308
# в зависимости от наличия в файле аудио/видео дорожек
get_cur_pos_info()
{
local pos_info=`cat $tmp_file | head --lines $end_line | tail --lines $((end_line - start_line + 1)) | tr '\33\15' '\n' \
| egrep '^[AV]:[[:space:]]*[[:digit:]]+\.[[:digit:]]+[[:space:]]+' \
| tail --lines 1 \
| sed -r 's/:\s+/:/g' | sed -r 's/\s+/ /g'`

if [[ $pos_info == "" ]]
then
return 1
fi

# Видео со звуком
if echo "$pos_info" | egrep -o '^A:[[:digit:]]+\.[[:digit:]]+ V:[[:digit:]]+\.[[:digit:]]' > /dev/null
then
pos_info=`echo -n "$pos_info" | awk '{ print $2 }' | awk -F '.' '{ print $1 }'`
# Видео без звука
elif echo "$pos_info" | egrep -o '^V:[[:digit:]]+\.[[:digit:]]' > /dev/null
then
pos_info=`echo -n "$pos_info" | awk -F '.' '{ print $1 }'`
# Аудио без видео
elif echo "$pos_info" | egrep -o '^A:[[:digit:]]+\.[[:digit:]]' > /dev/null
then
pos_info=`echo -n "$pos_info" | awk -F '.' '{ print $1 }'`
# Логическая ошибка
else
die "Logical error! :)"
fi

if [[ $pos_info == "" ]]
then
die "Logical error! :)"
fi

echo -n "$pos_info"
}


get_resume_pos()
{
local resume_info resume_info_time resume_info_time

# Устанавливаем разделитель слов равным \n
local IFS=$'\n'

for resume_info in $(< "$resume_info_file")
do
resume_info_time=`echo "$resume_info" | egrep '^.+:[[:digit:]]+$' | sed -r 's/^.+://' | egrep '^[[:digit:]]+$'`
resume_info_name=`echo "$resume_info" | sed "s/:${resume_info_time}\$//"`

# Пропускаем неверно сформированные записи
if [[ "$resume_info_time" == "" || "$resume_info_name" == "" ]]
then
# Предупреждение не выводим, т. к. оно будет выведено при записи в файл.
continue
fi

# Если это тот файл, который мы ищем
if [[ "$resume_info_name" == "$1" ]]
then
echo $resume_info_time
return 0
fi
done

return 1
}


if ! tmp_file=`mktemp`
then
die "Can't create temp file."
fi

if ! which mplayer > /dev/null
then
die "Error! Mplayer not installed."
fi

if [[ ! -e "$resume_info_file" ]]
then
touch "$resume_info_file" || die
fi

# Изменяем агрументы, переданные mplayer'у так, чтобы выбранные видеофайлы
# воспроизводились с того момента, где в прошлый раз было прервано
# воспроизведение.
i=0
for option
do
options[$((i++))]="$option"

if [[ ${option:0:1} != '-' ]]
then
# Если значение параметра похоже на имя файла, то считаем, что
# требуется проиграть этот файл.  Если это просто значение опции, то
# скрипт все равно сработает нормально, разве что добавится лишний
# ключ -ss в случае когда значение параметра будет совпадать с
# именем какого-либо файла в системе.
if [[ -e "$option" ]]
then
if video_name=`get_video_name_by_file_name "$option"`
then
# Если воспроизведение этого видео файла было прервано ранее
if video_resume_pos=`get_resume_pos "$video_name"`
then
options[$((i++))]='-ss'
options[$((i++))]="$video_resume_pos"
fi
fi
fi
fi
done

# Запускаем mplayer с измененными параметрами командной строки
mplayer_ext_echo "Starting mplayer: mplayer ${options[@]}"
mplayer "${options[@]}" | tee "$tmp_file"

# Получаем все файлы, которые проигрывал mplayer -->
files_in_output="`egrep --line-number 'Playing[[:space:]]+.+\.' $tmp_file`"

for line in `seq \`echo "$files_in_output" | wc --lines\``
do
file_in_output=`echo "$files_in_output" | head --lines $line | tail --lines 1`
files_lines[$((line-1))]=`echo "$file_in_output" | awk -F ':' '{ print $1 }'`
files_paths[$((line-1))]=`echo "$file_in_output" | sed -r 's/^[[:digit:]]+:Playing[[:space:]]+//' | sed 's/\.$//g'`
done
# Получаем все файлы, которые проигрывал mplayer <--

# Получаем всю необходимую информацию о каждом проигранном файле -->
for num in `seq 1 ${#files_lines[*]}`
do
i=$((num-1))

start_line=${files_lines[$i]}

# Генерируем имя видео по имени файла
if ! video_name=`get_video_name_by_file_name "${files_paths[$i]}"`
then
mplayer_ext_echo "Skiping file '${files_paths[$i]}'"
continue
fi

# Если файл последний
if [[ $num -eq ${#files_lines[*]} ]]
then
end_line=$((`cat $tmp_file | wc --lines` + 1))

# Получаем строку со временем, на котором остановилось воспроизведение
if ! video_resume_string=`get_cur_pos_info`
then
# Получить строку не удалось - это может произойти по разным причинам,
# например, если не удалось открыть файл.
# Т. к. файл не проигрывался, то не запоминаем его позицию.
mplayer_ext_echo "Skiping file '${files_paths[$i]}'"
continue
fi

# Файл проигрался до конца
if [[ `cat $tmp_file | tail --lines 1` == 'Exiting... (End of file)' ]]
then
set_resume_pos "$video_name" 0
# Проигрывание файла было прервано
else
video_resume_time=`echo -n "$video_resume_string" | awk -F ':' '{ print $2 }'`

# Видео (для аудио отматывать не надо)
if [[ `echo -n "$video_resume_string" | awk -F ':' '{ print $1 }'` == 'V' ]]
then
# "Отматываем" видео назад (приблизительно) до предыдущего ключевого кадра
if [[ $((video_resume_time - keyint)) -lt 0 ]]
then
video_resume_time=0
else
video_resume_time=$((video_resume_time - keyint))
fi
fi

set_resume_pos "$video_name" "$video_resume_time"
fi
# Файл не последний
else
end_line=${files_lines[$((i+1))]}

# Получаем строку со временем, на котором остановилось воспроизведение
if ! video_resume_string=`get_cur_pos_info`
then
# Получить строку не удалось - это может произойти по разным причинам,
# например, если не удалось открыть файл.
# Т. к. файл не проигрывался, то не запоминаем его позицию.
mplayer_ext_echo "Skiping file '${files_names[$i]}'"
continue
fi

set_resume_pos "$video_name" 0
fi
done
# Получаем всю необходимую информацию о каждом проигранном файле <--

cleanup
Также скачать скрипт можно здесь.

21 комментарий:

Анонимный комментирует...

А нет ли решения для подобного всего, но для VLC?

sash-kan комментирует...

весьма полезная вещь.
обязательно опробую.

Dmitry Konishchev комментирует...

ansate, VLC "менее консольный". С ним без правки исходного кода такое вряд ли получится. Мой скрипт читает вывод MPlayer, в котором содержится текущая позиция проигрываемого файла, а потом при запуске MPlayer'a меняет аргументы, переданные пользователем MPlayer'у, так, чтобы он начал воспроизведение с того момента, на котором в прошлый раз остановился. VLC же ничего не выводит на консоль и не принимает никаких аргументов, которыми можно было бы менять начальную точку воспроизведения.

Анонимный комментирует...

Супер, сам порывался написать такое пару раз. Обломался после парсера вывода.

Причем, код внятный и гладкий.

Спасибо за скрипт :))

Dmitry Konishchev комментирует...

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

Pento комментирует...

Большое спасибо за скрипт!

Анонимный комментирует...

Против деб-пакета в локальном репозитории не будет возражений? Онлайнового репозитория у меня пока нет.

Анонимный комментирует...

И большое спасибо!

Dmitry Konishchev комментирует...

andfom, да нет, конечно, какие могут быть возражения. :)

Андрей комментирует...

Спасибо, помогло!

Анонимный комментирует...

Большое спасибо! Но, однако, стоило бы выложить скриптик отдельным файлом - у меня после копипаста он упорно не хотел работать, спасло perl -pi.bk -e 's/\r//' mplayer.ext

Dmitry Konishchev комментирует...

stanislav, ок, спасибо, добавил ссылку на скачивание.

Анонимный комментирует...

smplayer все запоминает.

Dmitry Konishchev комментирует...

> smplayer все запоминает.
Да, но я предпочитаю оригинальный MPlayer. :)

Анонимный комментирует...

Никак не могу добиться работы скрипта.При выходе пишет:
Выходим... (Выход)
mplayer.ext: Skiping file ''
Естественно ничего не запоминает.

Dmitry Konishchev комментирует...

Анонимный, мне довольно сложно судить о том, что у вас происходит, если вы не предоставите какие-либо данные. Какие аргументы вы передаете скрипту? Также я бы посмотрел на вывод "bash -x script ...".

Анонимный комментирует...

Вот здесь http://dpaste.com/254045/ запуск и остановка.
Здесь http://dpaste.com/254050/ вывод Вами указанной команды

Анонимный комментирует...

Вторая паста видимо неверная,вот:
http://dpaste.com/254055/

Dmitry Konishchev комментирует...

Анонимный, ну ничего себе. :) А что это у вас за дистрибутив такой, что mplayer в нем говорит по-русски? Первый раз такое вижу. Мой скрипт грепает вывод mplayer'а по английским фразам, который тот выдает в stdout. Попробуйте запустить его как "LC_ALL=C script ..." - в таком случае mplayer должен переключиться в режим вывода всех сообщений на английском языке.

Анонимный комментирует...

Дистрибутив Gentoo. Вообщем разобрался.Система собрана с LINGUAS="ru en". Mplayer при компиляции берёт первое значение переменной,остальное его не интересует.Запуск с подстановкой значений переменных ничего не даёт.Пересобрал mplayer с LINGUAS="en" и только тогда он заговорил по английски.Скрипт заработал.
Большое спасибо за прекрасный скрипт и помощь!

FreedoM комментирует...

Спасибо за скрипт))) mplayer2 в арче, кстати, тоже по-русски разговаривает, поставил первый, все заработало нормально =)) А то smplayer юзать под опенбоксом из за функции запоминания воспроизведения мне совсем не нравилось....