пятница, 29 ноября 2013 г.

FMX.Media.TMediaPlayer или пишем свой mp3-плеер для Android'а #2

В предыдущей статье (FMX.Media.TMediaPlayer или пишем свой mp3-плеер для Android'а #1) я показал вам, как создать mp3 плеер с минимальным функционалом для Android’а и сказал, что возможно добавлю ещё несколько функций. В этой статье я представляю вам почти законченный плеер, о том почему «почти» я расскажу ниже.

Что нового:
  • Добавил кнопку «повторять все треки»
  • Добавил кнопку «воспроизводить случайный трек»
  • В коде: Вкл. и откл. кнопок управления плеером в зависимости от состояния
  • Чтение тегов ID3v1, ID3v2 (Название трека, Автор трека, Альбом, Жанр)
  • В коде: Таб по умолчанию
  • В коде: Обработка события FormKeyUp для кнопки "Назад"
  • Label c длительностью трека разделил на два Label’а и перенёс под трекбар
Upd (23.04.14). Проверено на Delphi XE6

Изменения коснулись (если сравнивать с предыдущей версией):
  • change - интерфейса (трекбар, кнопки)
  • change - PlayNextClick
  • change - StopClear
  • change - PlayClick
  • new - ID3tags
  • change - Create
  • new - FormKeyUp
  • change - Timer1Timer
Структура проекта:

Project Manager:

Свойства всех элементов (изменения выделены другим цветом):

Как проект выглядит в Delphi XE5 UPD1:

Думаю нужно объяснить некоторые моменты…

1) Почему я поместил TImage на SpeedButton и где я взял стандартные иконки?

К сожалению, в свойстве StyleLookup пока нет всех стандартных иконок (заготовок для создаваемых кнопок), а иконки необходимы, поэтому было решено сделать это при помощи TImage. Пак стандартных иконок для Android’а можно забрать тут: http://developer.android.com/design/style/iconography.html

2) Как реализовано чтение тегов ID3v1, ID3v2?

Всё достаточно просто. Я не стал заморачиваться, и писать «велосипед», а взял готовый компонент «ID3v2 Library». Он позволяет читать ID3v1, Lyrics3v2, ID3v2.2, ID3v2.3 and ID3v2.4, но есть важное замечание: Использование данного компонента бесплатно, при условии что вы используете его в некоммерческих целях (т.е. вы не собираетесь продавать приложение, которое включает в себя этот компонент). Если же вы хотите использовать его в коммерческом приложении, то приобретайте лицензию! 

3) Зачем перенёс/разделил Label с длительностью трека?

Удобнее перематывать трек, видя при этом текущее время проигрывания и длительность трека.

Код приложения:
{
  ******************************************************************************
  MP3 Player for Android
  Autor: Andrey Efimov (Contact: http://delphifmandroid.blogspot.ru/)
  Use only non-commercial purposes. Necessarily indicating the authorship.
  Используйте только в некоммерческих целях. Обязательно с указанием авторства.
  ******************************************************************************
}

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.TabControl,
  FMX.StdCtrls, FMX.ListView.Types, FMX.ListView, FMX.Layouts, FMX.Media,
  FMX.Objects;

type
  TForm1 = class(TForm)
    TabControl1: TTabControl;
    ToolBar1: TToolBar;
    TrackBar1: TTrackBar;
    TabItem1: TTabItem;
    TabItem2: TTabItem;
    ListView1: TListView;
    PlayPrev: TSpeedButton;
    PlayNext: TSpeedButton;
    Layout1: TLayout;
    Play: TSpeedButton;
    Pause: TSpeedButton;
    Stop: TSpeedButton;
    Label1: TLabel;
    Label2: TLabel;
    MediaPlayer1: TMediaPlayer;
    Timer1: TTimer;
    Layout2: TLayout;
    PlayIteration: TSpeedButton;
    PlayShuffle: TSpeedButton;
    ImageRepeat: TImage;
    ImageShuffle: TImage;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure ListView1Change(Sender: TObject);
    procedure PlayClick(Sender: TObject);
    procedure PauseClick(Sender: TObject);
    procedure StopClick(Sender: TObject);
    procedure PlayPrevClick(Sender: TObject);
    procedure PlayNextClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure TrackBar1Change(Sender: TObject);
    procedure FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
      Shift: TShiftState);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure StopClear();
    procedure ID3tags();
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses
  System.IOUtils, ID3v1Library, ID3v2Library;

var
  PauseTime: integer = 0; // Пауза
  ID3v1Tag: TID3v1Tag = nil;
  ID3v2Tag: TID3v2Tag = nil;

procedure TForm1.FormCreate(Sender: TObject);
var
  LList: TStringDynArray; // внутренний список файлов
  LItem: TListViewItem; // список треков в ListView1
  path: string; // папка в которой будем искать файлы
  i: Integer;
begin

{ // Определяем папку для поиска }
  if TDirectory.Exists('/storage/') then
     path := '/storage/'
  else path := '/sdcard/';

{ // Поиск mp3 файлов }
  try
    LList := TDirectory.GetFiles(path, '*.mp3', TSearchOption.soAllDirectories);
  except
    ShowMessage('Ошибка #1!');
    Exit;
  end;

{ // Обновляем список треков }
  ListView1.BeginUpdate;
  try
    for i := 0 to Length(LList) - 1 do
    begin
      LItem := ListView1.Items.Add;
      // Убираем расширение файла, оставляем имя файла
      LItem.Text := TPath.GetFileNameWithoutExtension(LList[I]);
      // Помещаем полный путь в Detail
      LItem.Detail := LList[I];
    end;
  finally
    ListView1.EndUpdate;
  end;

  // Устанавливаем таб по умолчанию
  TabControl1.ActiveTab := TabItem1;

  //* Create both Tag classes
  ID3v1Tag := TID3v1Tag.Create;
  ID3v2Tag := TID3v2Tag.Create;
end;

procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
  Shift: TShiftState);
begin
  if Key = vkHardwareBack then
  begin
    if TabControl1.ActiveTab = TabItem2 then
    begin
        TabControl1.ActiveTab := TabItem1;
        Key := 0;
    end;
  end;
end;

{ Вспомогательная процедура для чтения ID3 тегов}
procedure TForm1.ID3tags;
begin
  ID3v2Tag.LoadFromFile(ListView1.Items[ListView1.ItemIndex].Detail);
  if Length(Trim(ID3v2Tag.GetUnicodeText('TIT2'))) <> 0 then begin

      Label1.Text := 'Трек: ' + ID3v2Tag.GetUnicodeText('TIT2');

      if Length(Trim(ID3v2Tag.GetUnicodeText('TPE1'))) <> 0 then begin
        Label2.Text := 'Автор: ' + ID3v2Tag.GetUnicodeText('TPE1');
      end
      else begin
        Label2.Text := 'Автор: -';
      end;

      if Length(Trim(ID3v2Tag.GetUnicodeText('TALB'))) <> 0 then begin
        Label3.Text := 'Альбом: ' + ID3v2Tag.GetUnicodeText('TALB');
      end
      else begin
        Label3.Text := 'Альбом: -';
      end;

      if Length(Trim(ID3v2Tag.GetUnicodeText('TCON'))) <> 0 then begin
        Label4.Text := 'Жанр: ' + ID3v2Tag.GetUnicodeText('TCON');
      end
      else begin
        Label4.Text := 'Жанр: -';
      end;
  end
  else begin

    ID3v1Tag.LoadFromFile(ListView1.Items[ListView1.ItemIndex].Detail);
    if Length(Trim(ID3v1Tag.Title)) <> 0 then begin

      Label1.Text := 'Трек: ' + Trim(ID3v1Tag.Title);

      if Length(Trim(ID3v1Tag.Artist)) <> 0 then begin
        Label2.Text := 'Автор: ' + Trim(ID3v1Tag.Artist);
      end
      else begin
        Label2.Text := 'Автор: -';
      end;

      if Length(Trim(ID3v1Tag.Album)) <> 0 then begin
        Label3.Text := 'Альбом: ' + Trim(ID3v1Tag.Album);
      end
      else begin
        Label3.Text := 'Альбом: -';
      end;

      if Length(Trim(ID3v1Tag.Album)) <> 0 then begin
        Label4.Text := 'Жанр: ' + Trim(ID3v1Tag.Genre);
      end
      else begin
        Label4.Text := 'Жанр: -';
      end;
    end
    else begin
      Label1.Text := 'Трек: ' + ListView1.Items[ListView1.ItemIndex].Text;
      Label2.Text := 'Автор: -';
      Label3.Text := 'Альбом: -';
      Label4.Text := 'Жанр: -';
    end;

  end;

end;

{ Список треков: Выбор трека }
procedure TForm1.ListView1Change(Sender: TObject);
begin
  PlayClick(Self);
end;

{ Кнопка: Pause }
procedure TForm1.PauseClick(Sender: TObject);
begin
  if (MediaPlayer1.State = TMediaState.Playing) then
  begin
    // Запоминаем текущее время воспроизведения трека
    PauseTime := MediaPlayer1.Media.CurrentTime;
    // Останавливаем проигрывание
    MediaPlayer1.Stop;
  end;
end;

{ Кнопка: Play }
procedure TForm1.PlayClick(Sender: TObject);
begin
  if ListView1.ItemIndex <> -1 then
  begin
    // Выключаем таймер
    Timer1.Enabled := False;

    MediaPlayer1.Clear;
    MediaPlayer1.FileName := ListView1.Items[ListView1.ItemIndex].Detail;

    // Трекбар для перемотки
    TrackBar1.Enabled := True;
    TrackBar1.Max := MediaPlayer1.Duration;

    // Проверка на Паузу
    if (MediaPlayer1.State = TMediaState.Stopped) AND (PauseTime <> 0) then
    begin
      MediaPlayer1.CurrentTime := PauseTime;
      PauseTime := 0;
    end
    else begin
      // Текущая позиция трекбара
      TrackBar1.Value := 0;
    end;

    MediaPlayer1.Play;

    // читаем теги
    ID3tags;

    // Включаем таймер
    Timer1.Enabled := True;

    // Включаем кнопки
    PlayPrev.Enabled := True;
    Play.Enabled := True;
    Pause.Enabled := True;
    Stop.Enabled := True;
    PlayNext.Enabled := True;

  end;
end;

{ Кнопка: Следующий трек }
procedure TForm1.PlayNextClick(Sender: TObject);
begin
  if (ListView1.ItemIndex <> -1) AND (ListView1.ItemIndex < ListView1.Items.Count-1) then
  begin
    if PlayShuffle.IsPressed = False then begin
      ListView1.ItemIndex := ListView1.ItemIndex + 1;
      PauseTime := 0;
      PlayClick(Self);
    end
    else begin
      Randomize;
      ListView1.ItemIndex := Random(ListView1.Items.Count - 1);
      PauseTime := 0;
      PlayClick(Self);
    end;
  end
  else begin
    if (PlayIteration.IsPressed = True) OR ((PlayIteration.IsPressed = False) AND (PlayShuffle.IsPressed = True)) then
    begin
      ListView1.ItemIndex := ListView1.ItemIndex - (ListView1.Items.Count - 1);
      PauseTime := 0;
      PlayClick(Self);
    end
    else begin
      StopClear;
    end;
  end;
end;

{ Кнопка: Предыдущий трек }
procedure TForm1.PlayPrevClick(Sender: TObject);
begin
  if (ListView1.ItemIndex <> -1) AND (ListView1.ItemIndex > 0) then
  begin
    ListView1.ItemIndex := ListView1.ItemIndex - 1;
    PauseTime := 0;
    PlayClick(Self);
  end
  else begin
    StopClear;
  end;
end;

{ Вспомогательная процедура }
procedure TForm1.StopClear;
begin
    MediaPlayer1.Stop;
    Timer1.Enabled := False;
    ListView1.ItemIndex := -1;
    TrackBar1.Value := 0;
    TrackBar1.Enabled := False;
    PauseTime := 0;

    // Включаем кнопки
    PlayPrev.Enabled := False;
    Play.Enabled := False;
    Pause.Enabled := False;
    Stop.Enabled := False;
    PlayNext.Enabled := False;
end;

{ Кнопка: Stop }
procedure TForm1.StopClick(Sender: TObject);
begin
  if (MediaPlayer1.State = TMediaState.Playing) then
  begin
    StopClear;
  end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
var
  CurrentMin, CurrentSec, DurationMin, DurationSec: integer;
begin
  // Меняем позицию трекбара на текущую позицию воспроизведения
  if (MediaPlayer1.State = TMediaState.Playing) then
  begin
    TrackBar1.Tag := 1;
    TrackBar1.Value := MediaPlayer1.CurrentTime;
    TrackBar1.Tag := 0;

    CurrentMin := MediaPlayer1.CurrentTime div 1000 div 60;
    CurrentSec := MediaPlayer1.CurrentTime div 1000 mod 60;
    DurationMin := MediaPlayer1.Duration div 1000 div 60;
    DurationSec := MediaPlayer1.Duration div 1000 mod 60;
    Label5.Text := Format('%2.2d:%2.2d', [CurrentMin, CurrentSec]);
    Label6.Text := Format('%2.2d:%2.2d', [DurationMin, DurationSec]);
  end;

  //Если трек завершился, то начинаем играть следующий
  if (Round((MediaPlayer1.CurrentTime/MediaPlayer1.Duration)*100) = 100) then
  begin
    PlayNextClick(Self);
  end;
end;

procedure TForm1.TrackBar1Change(Sender: TObject);
begin
  // Используем свойство Tag как флаг, чтобы избежать одновременное обновление
  // трекбара и MediaPlayer1.CurrentTime при воспроизведении
  if TrackBar1.Tag = 0 then
  begin
    // Проверка на Паузу
    if (MediaPlayer1.State = TMediaState.Stopped) AND (PauseTime <> 0) then
    begin
      PauseTime := Round(TrackBar1.Value);
    end
    else
    begin
      MediaPlayer1.CurrentTime := Round(TrackBar1.Value);
    end;
  end;
end;

end.

А теперь объясню, почему приложение почти законченно… Я хотел добавить функцию поиска по ListView, но когда добавил, обнаружил сразу 2 бага (один из них приводит к краху приложения). О багах я сообщил в Embarcadero, теперь надо ждать, когда исправят и выпустят обновление. С поиском намного удобнее просматривать список в 523 строки. Также я сообщил о медленной скорости скролла в ListView.

Кстати плеер отлично работает в фоне (проверено на SGS2) и должен одинаково отображаться на планшете и телефоне.

Это последний пост, посвящённый стандартному компоненту TMediaPlayer, т.к. больше из него уже не выжать:). В будущем может быть ещё напишем плеер, с использованием сторонней библиотеки и так чтобы всё умел (проигрывание, эквалайзер, анализ спектра и т.д.).

Вот идея для Embarcadero, за каждый найденный баг продлевать триал на неделю/месяц.


Если у вас появились вопросы, не стесняйтесь задавать их в комментариях.

Исходник (mp3player2.zip)
Apk файл для установки: Скачать с Google Drive (Delphi XE6)

10 комментариев:

  1. Мне кажется, что код удобнее выкладывать на стороннем сервере.
    В github репозитории или в виде gists.

    ОтветитьУдалить
    Ответы
    1. Да, возможно вы правы. Обязательно подумаю над этим. Просто блог изначально планировался в личных целях, а потом начал народ приходить и пришлось потихоньку приводить его в порядок :)
      p.s. За совет спасибо.

      Удалить
  2. Ответы
    1. Добавил apk файл для установки.

      Удалить
  3. А как TMediaPlayer заставить проигрывать еще и flac?

    ОтветитьУдалить
    Ответы
    1. Вроде можно добавлять свои кодеки, надо уточнять этот момент в справке. Можно использовать другие библиотеки для проигрывания форматов, которые не поддерживаются стандартно.
      Про поддерживаемые форматы (по умолчанию) можно почитать тут: http://delphifmandroid.blogspot.ru/2013/11/delphi-xe5-ios-android.html

      Удалить
  4. Великие, поздравления,
    Интересно, если кто знает, как установить песню в качестве рингтона / звон?

    Спасибо.

    ОтветитьУдалить
  5. Не работает. Список файлов пуст.

    ОтветитьУдалить
    Ответы
    1. Какое устройство, какая версия ОС?

      Удалить
  6. Почему то не воспроизводится и не снимаются enable кнопки.

    ОтветитьУдалить