classdef mmplayer < handle
%MMPLAYER Video Player for Multimedia Reader Objects (mmreader)
%
% MMPLAYER(OBJ) creates a new media player object and
% opens a new media player GUI playing the frames
% of a given mmreader object OBJ.
% the player can either be controlled using its GUI or remotely by
% method calls.
% it also features various events and properties allowing the user
% to keep track of the player state and playback process.
% note: MMPLAYER is NOT designed for fast and smooth video playback.
% Depending on your machine it will play low resolution videos
% without any problems though.
%
% METHODS:
% play()
% starts playback mode. In playback mode you will not be able
% to use the GUI navigation features, especially the slider.
%
% pause()
% pauses playback mode at current position.
%
% stop()
% stops playback mode and jumps back to the beginning of the
% video.
%
% toggle()
% toggles between playing- and pause mode.
%
% stepForward()
% steps 1 frame forward.
%
% stepBack()
% steps 1 frame back.
%
% jump(INDEX)
% jumps to frame INDEX.
%
% axesClear()
% clears all objects from the output axes except the actual
% video frame. (see: axesHandle-property)
% note: this will NOT happen automatically after a new frame
% is shown.
%
% delete()
% object deconstructor. will also close the GUI.
%
% PROPERTIES:
% fps (read-only)
% frames per second (specified by mmreader object)
%
% width (read-only)
% video width in pixels (specified by mmreader object)
%
% height (read-only)
% video height in pixels (specified by mmreader object)
%
% numberOfFrames;
% total number of video frames (specified by mmreader object)
%
% duration (read-only)
% video duration in seconds (specified by mmreader object)
%
% currentFrame (read-only)
% current video frame as RGB image matrix
%
% axesHandle (read-only)
% handle to axes the image is drawn on. can be used to add
% custom drawings to video frames in the player.
% (see: axesClear-method)
%
% repeat (logical)
% gets / sets repeat mode. if set to TRUE, the player will
% continue playback from the beginning after reaching the end
% of the video.
%
% scaling
% gets / sets scaling mode:
% 'stretch' stretches the video to fit the window size.
%
% 'ratio' scales video to fit window size but always
% (default) keeps the original aspect ratio.
%
% 'off' turns off scaling (1:1 mode).
%
% playing (logical)
% gets / sets playback mode. if changed, playback will be
% started / stopped automatically.
%
% currentFrameIndex (numeric)
% gets / sets current frame index. If changed, player jumps
% to the new frame automatically.
%
% verbose (logical)
% gets / sets text output behavior. If set, the player will
% print out additional information.
%
% EVENTS:
% refreshed
% notifies that a frame was rendered. (exmaple below)
%
% started
% notifies that playback mode was started (after calling
% start-method or starting using the GUI).
%
% stopped
% notifies that playback mode was stopped. (after calling
% stop-method or stopping using the GUI).
%
% paused
% notifies that playback mode was paused. (after calling
% pause-method or pausing using the GUI).
%
% GUI:
% - control playback using the menu / keyboard shortcuts / slider
% - slider navigation: step frame-wise by clicking the
% arrow-buttons and second-wise by clicking the slider bar
% - status bar shows playback status / frame index / time
% - note: control features of the GUI are limited in playback
% mode
%
% EXAMPLE: basic usage
% obj = mmreader('video.mpg');
% player = mmplayer(obj);
%
% EXAMPLE: event listener
% addlistener(player,'refreshed',@(src,event)disp(src.currentFrameIndex));
% player.play
% Copyright 2009 Deutsche Telekom Laboratories
% Author: Robert Walter (rwalter83@gmail.com)
% Co-Author: Robert Schleicher (robert.schleicher@telekom.de)
% $Revision: $ $Date: $
% TODO
% - openGL renderer
% - buffered frame acquisition (speed boost expected)
% - sound playback
%% constants
properties (Hidden = true, Constant)
% ui properties
controlHeight = 20;
statusHeight = 15;
end
%% private properties
properties (Hidden = true)
% handles
hfig;
haxes;
himage;
htimerRefresh;
htimerNextFrame;
huicslider;
huipcontrol;
huipstatus;
huipvideo;
huimloop;
huimstepback;
huimstepforward;
huimjump;
huimscalingoff;
huimscalingratio;
huimscalingstretch;
huicstatus;
huicframe;
huictime;
mmreaderObj;
% misc
deletedFlag;
playingFlag;
durationStr;
currentFrameNo;
privScaling;
end
%% read-only properties
properties (SetAccess = private)
% general
fps;
width;
height;
numberOfFrames;
duration;
currentFrame;
axesHandle;
end
%% public properties
properties
repeat;
scaling;
playing;
currentFrameIndex;
verbose;
end
events
refreshed
started
stopped
paused
end
methods (Hidden = true, Static)
function str = time2str(seconds)
str = datestr( seconds / (24*3600),'HH:MM:SS' );
end
end
methods (Hidden = true)
% timer creation
function obj = createTimers(obj)
% frame counter
obj.htimerNextFrame = timer(...
'UserData',obj,...
'ExecutionMode','fixedRate',...
'BusyMode','queue',...
'Period',1/obj.fps,...
'TimerFcn',@(src,event)nextFrame(obj));
% refresh
obj.htimerRefresh = timer(...
'UserData',obj,...
'ExecutionMode','fixedSpacing',...
'BusyMode','drop',...
'Period',1/obj.fps,... %leave 1/fps seconds time for other operations (refresh-rate ~= 1/2 fps)
'TimerFcn',@(src,event)refresh(obj,true));
end
% GUI creation
function obj = createGui(obj)
%opengl hardware
%opengl info
% create main figure
obj.hfig = figure(...
'NumberTitle','off', ...
'ResizeFcn',@(src,event)alignControls(obj),...
'MenuBar','none',...
'Renderer','painter',...
'Name', sprintf('Multimedia Video Player: %s',get(obj.mmreaderObj,'Name')) ,...
'DeleteFcn',@(src,event)delete(obj));
% add menu
huimfile = uimenu(obj.hfig,...
'Label','File');
uimenu(huimfile,...
'Label','Exit',...
'Separator','on',...
'Accelerator','Q',...
'Callback',@(src,event)delete(obj));
huimplayback = uimenu(obj.hfig,...
'Label','Playback');
uimenu(huimplayback,...
'Label','Play/Pause',...
'Accelerator','P',...
'Callback',@(src,event)toggle(obj));
uimenu(huimplayback,...
'Label','Stop',...
'Accelerator','S',...
'Callback',@(src,event)stop(obj));
obj.huimstepback = uimenu(huimplayback,...
'Label','Step Back',...
'Separator','on',...
'Accelerator','B',...
'Callback',@(src,event)stepBack(obj));
obj.huimstepforward = uimenu(huimplayback,...
'Label','Step Forward',...
'Accelerator','F',...
'Callback',@(src,event)stepForward(obj));
function frame = frameNoDlg(default)
prompt={'Enter frame number'};
name='Input';
numlines=1;
defaultanswer={num2str(default)};
answer=inputdlg(prompt, name, numlines, defaultanswer);
frame = uint32(str2double(cell2mat(answer)));
end
obj.huimjump = uimenu(huimplayback,...
'Label','Jump to Frame',...
'Accelerator','J',...
'Interruptible','on',...
'Callback',@(src,event)jump(obj, frameNoDlg(obj.currentFrameNo) ));
function toggleLoop(obj)
obj.repeat = ~obj.repeat;
end
obj.huimloop = uimenu(huimplayback,...
'Label','Loop',...
'Separator','on',...
'Checked','on',...
'Callback',@(src,event)toggleLoop(obj));
huimview = uimenu(obj.hfig,...
'Label','View');
function setScaling(obj,mode)
obj.scaling = mode;
end
obj.huimscalingoff = uimenu(huimview,...
'Label','No Scaling',...
'Callback',@(src,event)setScaling(obj,'off'));
obj.huimscalingratio = uimenu(huimview,...
'Label','Keep Aspect Ratio',...
'Callback',@(src,event)setScaling(obj,'ratio'));
obj.huimscalingstretch = uimenu(huimview,...
'Label','Stretch fo fit Window',...
'Callback',@(src,event)setScaling(obj,'stretch'));
% add controls
obj.huipcontrol = uipanel(...
'BorderType','line',...
'Units','pixels',...
'Parent',obj.hfig);
obj.huicslider = uicontrol(...
'Parent',obj.huipcontrol,...
'Units','normalized',...
'Position',[0 0 1 1],...
'Max',obj.numberOfFrames,...
'Value',1,...
'SliderStep',[1/(obj.numberOfFrames-1) obj.fps/(obj.numberOfFrames-1)],...
'Min',1,...
'Callback',@(src,event)obj.jumpToFrame( uint32(get(src,'Value')) , false ),...
'Style','slider');
obj.huipstatus = uipanel(...
'BorderType','none',...
'BackgroundColor','black',...
'Units','pixels',...
'Parent',obj.hfig);
obj.huicstatus = uicontrol(...
'Parent',obj.huipstatus,...
'BackgroundColor','black',...
'ForegroundColor','white',...
'HorizontalAlignment','left',...
'Style','text',...
'Units','normalized',...
'Position',[0 0 1/3 1]);
obj.huicframe = uicontrol(...
'Parent',obj.huipstatus,...
'BackgroundColor','black',...
'ForegroundColor','white',...
'HorizontalAlignment','center',...
'Style','text',...
'Units','normalized',...
'Position',[1/3 0 1/3 1]);
obj.huictime = uicontrol(...
'Parent',obj.huipstatus,...
'Style','text',...
'BackgroundColor','black',...
'ForegroundColor','white',...
'HorizontalAlignment','right',...
'Units','normalized',...
'Position',[2/3 0 1/3 1]);
obj.huipvideo = uipanel(...
'BorderType','none',...
'BackgroundColor','black',...
'Units','pixels',...
'Parent',obj.hfig);
obj.haxes = axes(...
'Parent',obj.huipvideo,...
'Visible','off',...
'DataAspectRatio',[1 1 1],...
'DrawMode','fast',...
'XLimMode','manual',...
'XLim',[1 obj.width],...
'YLimMode','manual',...
'YLim',[1 obj.height],...
'Units','normalized',...
'Position',[0 0 1 1],...
'YDir','reverse');
obj.himage = image(...
'Parent',obj.haxes,...
'CData',[]);
end
% timer callback: increments frame counter
function nextFrame(obj)
if isvalid(obj) % valid handle
if ~obj.setFrame(obj.currentFrameNo + 1); % invalid frame
if obj.repeat
obj.setFrame(1);
else
obj.stop;
end
end
end
end
% timer callback: loads and renders current frame
function refresh(obj,controls)
index = obj.currentFrameNo;
% load frame from video file
if isvalid(obj)
% throws some errors once in a while .. whatever
try
obj.currentFrame = read(obj.mmreaderObj, index); % produces big delay
catch error % TODO give a message
if isvalid(obj) && obj.verbose % TODO : Operands to the || and && operators must be convertible to logical scalar values.
disp(['read error: ' error.message]);
end
% * MATLAB:read:readTimedOut
% * The frame index requested is beyond EOF
return;
end
end
% update frame+status and notify user
if isvalid(obj)
% update image
set(obj.himage, 'CData', obj.currentFrame);
% update status
setStatusFrame(obj, index);
setStatusTime(obj, double(index) / obj.fps);
% update controls (if requested)
if controls
setControlFrame(obj, index);
end
% notify user
notify(obj,'refreshed');
end
end
% sets frames respecting boundaries
function valid = setFrame(obj,frame)
valid = frame >= 1 && frame <= obj.numberOfFrames;
if valid
obj.currentFrameNo = frame;
end
end
% status bar update
function setStatusText(obj, status)
set(obj.huicstatus,'String',[' ' status]);
end
function setStatusTime(obj, time)
str = sprintf('%s / %s', mmplayer.time2str(time), obj.durationStr);
set(obj.huictime, 'String', [str ' ']);
end
function setStatusFrame(obj, frame)
set(obj.huicframe,'String',sprintf('%d / %d', [frame obj.numberOfFrames]));
end
% updates slider position
function setControlFrame(obj, frame)
set(obj.huicslider,'Value',frame);
end
% automatic alignment on resize
function alignControls(obj)
posFig = get(obj.hfig,'Position');
widthFig = posFig(3);
heightFig = posFig(4);
% align panels
set(obj.huipstatus,'Position',[0 0 widthFig obj.statusHeight]);
set(obj.huipcontrol,'Position',[0 obj.statusHeight widthFig obj.controlHeight]);
set(obj.huipvideo,'Position',[0 obj.controlHeight+obj.statusHeight widthFig heightFig-(obj.controlHeight+obj.statusHeight)]);
posParent = get(get(obj.haxes,'Parent'), 'Position');
widthParent = posParent(3);
heightParent = posParent(4);
% align axes
if isequal(obj.privScaling, 'off')
set(obj.haxes, 'Position', [(widthParent-obj.width)/2 (heightParent-obj.height)/2 obj.width obj.height]);
elseif isequal(obj.privScaling, 'stretch')
set(obj.haxes, 'DataAspectRatio', [obj.width/widthParent obj.height/heightParent 1]);
set(obj.haxes, 'Position', [1 1 widthParent heightParent]);
end
end
% frame jump
function jumpToFrame(obj, frame, refreshControls)
obj.setFrame(frame);
if ~obj.playingFlag
obj.refresh(refreshControls);
end
end
% enables/disable controls for navigation/playback mode
function disableControls(obj)
set(obj.huicslider,'Enable','inactive');
set(obj.huimstepback,'Enable','off');
set(obj.huimstepforward,'Enable','off');
set(obj.huimjump,'Enable','off');
end
function enableControls(obj)
set(obj.huicslider,'Enable','on');
set(obj.huimstepback,'Enable','on');
set(obj.huimstepforward,'Enable','on');
set(obj.huimjump,'Enable','on');
end
end
methods
%% constructor
function obj = mmplayer(mmreaderObj)
% initialize proprties
obj.mmreaderObj = mmreaderObj;
obj.currentFrameNo = 1;
obj.deletedFlag = false;
obj.fps = get(mmreaderObj, 'FrameRate');
obj.width = get(mmreaderObj, 'Width');
obj.height = get(mmreaderObj, 'Height');
obj.numberOfFrames = get(mmreaderObj, 'NumberOfFrames');
obj.duration = get(mmreaderObj, 'Duration');
obj.durationStr = mmplayer.time2str(obj.duration);
obj.repeat = true;
obj.verbose = false;
% setup timers
obj = createTimers(obj);
% create GUI
obj = createGui(obj);
% init player
obj.scaling = 'ratio';
refresh(obj,true);
stop(obj);
%notify(obj,'created');
end
%% multimedia control
function play(obj)
start(obj.htimerRefresh);
start(obj.htimerNextFrame);
obj.playingFlag = true;
obj.disableControls();
setStatusText(obj,'Playing ...');
end
function pause(obj)
stop(obj.htimerRefresh);
stop(obj.htimerNextFrame);
obj.playingFlag = false;
obj.enableControls();
setStatusText(obj,'Paused');
notify(obj,'paused');
end
function toggle(obj)
if obj.playingFlag
obj.pause;
else
obj.play;
end
end
function stop(obj)
stop(obj.htimerRefresh);
stop(obj.htimerNextFrame);
obj.setFrame(1);
obj.playingFlag = false;
obj.refresh(true);
obj.enableControls();
obj.setStatusText('Stopped');
notify(obj,'stopped');
end
function stepForward(obj)
jump(obj,obj.currentFrameNo+1)
end
function stepBack(obj)
jump(obj,obj.currentFrameNo-1)
end
function jump(obj, frame)
obj.jumpToFrame(frame, true);
end
%% proprties
function set.repeat(obj, value)
if islogical(value)
obj.repeat = value;
if value
set(obj.huimloop, 'Checked', 'on');
else
set(obj.huimloop, 'Checked', 'off');
end
else
disp('invalid value')
end
end
function set.scaling(obj,value)
if ischar(value)
if isequal(value,'off') || isequal(value,'stretch') || isequal(value,'ratio')
set(obj.huimscalingoff, 'Checked', 'off');
set(obj.huimscalingratio, 'Checked', 'off');
set(obj.huimscalingstretch, 'Checked', 'off');
if isequal(value,'off')
obj.privScaling = value;
set(obj.haxes,'Units','pixels');
set(obj.haxes, 'DataAspectRatio', [1 1 1]);
set(obj.huimscalingoff, 'Checked', 'on');
elseif isequal(value,'stretch')
obj.privScaling = value;
set(obj.haxes,'Units','pixels');
set(obj.huimscalingstretch, 'Checked', 'on');
elseif isequal(value,'ratio')
obj.privScaling = value;
set(obj.haxes,'Units','normalized');
set(obj.haxes, 'DataAspectRatio', [1 1 1]);
set(obj.haxes,'Position',[0 0 1 1]);
set(obj.huimscalingratio, 'Checked', 'on');
end
else
disp('unknonw value! use off, ratio or stretch')
end
else
disp('invalid value');
end
obj.alignControls();
end
function set.playing(obj,value)
if islogical(value)
if value
obj.play();
else
obj.pause();
end
else
disp('invalid value');
end
end
function value = get.playing(obj)
value = obj.playingFlag;
end
function value = get.currentFrameIndex(obj)
value = obj.currentFrameNo;
end
function set.currentFrameIndex(obj,value)
if isnumeric(value)
obj.jump(value);
else
disp('invalid value');
end
end
function set.verbose(obj,value)
if islogical(value)
obj.verbose = value;
else
disp('invalid value');
end
end
%% axes
function value = get.axesHandle(obj)
hold(obj.haxes, 'on');
value = obj.haxes;
end
function axesClear(obj)
children = get(obj.haxes,'Children');
delete( children(children ~= obj.himage) );
end
%% deconstructor
function delete(obj)
if (~obj.deletedFlag)
%disp('delete');
stop(obj.htimerNextFrame);
stop(obj.htimerRefresh);
delete(obj.htimerNextFrame);
delete(obj.htimerRefresh);
obj.deletedFlag = true;
delete(obj.hfig);
end
end
end
end