001    /*
002     * 
003     * $Revision: 15022 $ $Date: 2009-03-26 14:53:00 +0100 (Thu, 26 Mar 2009) $
004     *
005     * This file is part of ***  M y C o R e  ***
006     * See http://www.mycore.de/ for details.
007     *
008     * This program is free software; you can use it, redistribute it
009     * and / or modify it under the terms of the GNU General Public License
010     * (GPL) as published by the Free Software Foundation; either version 2
011     * of the License or (at your option) any later version.
012     *
013     * This program is distributed in the hope that it will be useful, but
014     * WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program, in a file called gpl.txt or license.txt.
020     * If not, write to the Free Software Foundation Inc.,
021     * 59 Temple Place - Suite 330, Boston, MA  02111-1307 USA
022     */
023    
024    package org.mycore.datamodel.ifs;
025    
026    import java.io.ByteArrayOutputStream;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.io.OutputStream;
030    import java.net.MalformedURLException;
031    import java.net.URL;
032    import java.net.URLConnection;
033    import java.text.DecimalFormat;
034    
035    import org.mycore.common.MCRConfiguration;
036    import org.mycore.common.MCRConfigurationException;
037    import org.mycore.common.MCRException;
038    import org.mycore.common.MCRPersistenceException;
039    import org.mycore.common.MCRUtils;
040    
041    /**
042     * For MCRFiles that contain streaming audio/video, instances of this class
043     * provide methods to get technical metadata like bitrate, framerate, duration,
044     * size etc. and to start a player to stream the asset to a browser.
045     * 
046     * @author Frank Lützenkirchen
047     * @version $Revision: 15022 $ $Date: 2009-03-26 14:53:00 +0100 (Thu, 26 Mar 2009) $
048     */
049    public abstract class MCRAudioVideoExtender {
050        /** Constant for media type = video */
051        public final static boolean VIDEO = true;
052    
053        /** Constant for media type = audio */
054        public final static boolean AUDIO = false;
055    
056        /** The bitrate of the asset in number of bits per second */
057        protected int bitRate = 0;
058    
059        /** The framerate of the asset in number of frames per second */
060        protected double frameRate = 0;
061    
062        /** The media type of the asset, either AUDIO or VIDEO */
063        protected boolean mediaType = VIDEO;
064    
065        /** The hours part of the duration of the asset */
066        protected int durationHours = 0;
067    
068        /** The minutes part of the duration of the asset */
069        protected int durationMinutes = 0;
070    
071        /** The seconds part of the duration of the asset */
072        protected int durationSeconds = 0;
073    
074        /** The size of the asset in bytes */
075        protected long size = 0;
076    
077        /** The content type of the asset */
078        protected String contentTypeID = "unknown";
079    
080        /** The URL where clients can download a player for the asset */
081        protected String playerDownloadURL = "";
082    
083        /** The MIME type a servlet has to send with the player starter */
084        protected String playerStarterCT = "";
085    
086        /** The base URL where to get a metafile that starts a player in browser */
087        protected String basePlayerStarter = "";
088    
089        /** The base URL of a cgi that provides technical metadata about the asset */
090        protected String baseMetadata = "";
091    
092        /** The asset file this extender belongs to */
093        protected MCRFileReader file;
094    
095        /**
096         * Creates a new MCRAudioVideoExtender. The instance has to be initialized
097         * by invoking init() before it can be used.
098         */
099        public MCRAudioVideoExtender() {
100        }
101    
102        /**
103         * Initializes this AudioVideoExtender and gets technical metadata from the
104         * server that holds the streaming asset. Subclasses must override this
105         * method!
106         * 
107         * @param file
108         *            the MCRFile that this extender belongs to
109         */
110        public void init(MCRFileReader file) throws MCRException {
111            this.file = file;
112        }
113    
114        /**
115         * Returns the maximum number of bits per seconds when asset is streamed
116         * 
117         * @return the maximum number of bits per seconds when asset is streamed
118         */
119        public int getBitRate() {
120            return bitRate;
121        }
122    
123        /**
124         * Returns the streaming bitrate formatted as a String, e. g. "1.3 MBit" or
125         * "300 kBit".
126         * 
127         * @return the streaming bitrate formatted as a String
128         */
129        public String getBitRateFormatted() {
130            if (bitRate > (1024 * 1024)) {
131                double b = Math.round(bitRate / 10485.76) / 100.0;
132                return new DecimalFormat("##0.##").format(b) + " MBit";
133            }
134            double b = Math.round(bitRate / 102.4) / 10.0;
135            return new DecimalFormat("##0.#").format(b) + " kBit";
136        }
137    
138        /**
139         * Returns the maximum number of frames per second for a streaming video
140         * asset
141         * 
142         * @return the maximum number of frames per second for a streaming video
143         *         asset
144         */
145        public double getFrameRate() {
146            return frameRate;
147        }
148    
149        /**
150         * Returns the framerate formatted as a String, e. g. "25.0"
151         * 
152         * @return the framerate formatted as a String, e. g. "25.0"
153         */
154        public String getFrameRateFormatted() {
155            // double r = (double)( Math.round( frameRate * 10.0 ) ) / 10.0;
156            return new DecimalFormat("##.#").format(frameRate);
157        }
158    
159        /**
160         * Returns the media type, either AUDIO od VIDEO for this asset
161         * 
162         * @return the media type, compare to the boolean constants in this class
163         */
164        public boolean getMediaType() {
165            return mediaType;
166        }
167    
168        /**
169         * Returns true, if this asset is an audio asset.
170         * 
171         * @return true, if this asset is an audio asset.
172         */
173        public boolean isAudio() {
174            return (mediaType == AUDIO);
175        }
176    
177        /**
178         * Returns true, if this asset is a video asset.
179         * 
180         * @return true, if this asset is a video asset.
181         */
182        public boolean isVideo() {
183            return (mediaType == VIDEO);
184        }
185    
186        /**
187         * Returns the hours part of the duration of this asset
188         * 
189         * @return the hours part of the duration of this asset
190         */
191        public int getDurationHours() {
192            return durationHours;
193        }
194    
195        /**
196         * Returns the minutes part of the duration of this asset
197         * 
198         * @return the minutes part of the duration of this asset
199         */
200        public int getDurationMinutes() {
201            return durationMinutes;
202        }
203    
204        /**
205         * Returns the seconds part of the duration of this asset
206         * 
207         * @return the seconds part of the duration of this asset
208         */
209        public int getDurationSeconds() {
210            return durationSeconds;
211        }
212    
213        /**
214         * Returns the duration of this asset, formatted as a String for output. For
215         * example, <tt>getDurationFormatted( "Std.", "Min.", "Sek." )</tt> will
216         * return the String "1 Std. 15 Min." for an asset that is one hour and 15
217         * minutes long. If duration is less than one hour, only minutes and seconds
218         * go into the output string, otherwise hours and minutes are used.
219         * 
220         * @param hourLabel
221         *            the label for the hours part of the duration
222         * @param minutesLabel
223         *            the label for the minutes part of the duration
224         * @param secondsLabel
225         *            the label for the seconds part of the duration
226         * @return the duration of this asset, formatted as a String
227         */
228        public String getDurationFormatted(String hourLabel, String minutesLabel, String secondsLabel) {
229            StringBuffer sb = new StringBuffer();
230    
231            if (durationHours > 0) {
232                sb.append(durationHours);
233                sb.append(" ").append(hourLabel).append(" ");
234                sb.append(durationMinutes);
235                sb.append(" ").append(minutesLabel);
236            } else if (durationMinutes > 0) {
237                sb.append(durationMinutes);
238                sb.append(" ").append(minutesLabel).append(" ");
239                sb.append(durationSeconds);
240                sb.append(" ").append(secondsLabel);
241            } else {
242                sb.append(durationSeconds);
243                sb.append(" ").append(secondsLabel);
244            }
245    
246            return sb.toString();
247        }
248    
249        /**
250         * Returns the duration of the asset, formatted as a timcode, e. g.
251         * "01:15:00" for an asset thats duration is one hour and 15 minutes.
252         * 
253         * @return the duration foramatted as a timecode like "hh:mm:ss"
254         */
255        public String getDurationTimecode() {
256            DecimalFormat formatter = new DecimalFormat("00");
257            StringBuffer sb = new StringBuffer();
258            sb.append(formatter.format(durationHours));
259            sb.append(":");
260            sb.append(formatter.format(durationMinutes));
261            sb.append(":");
262            sb.append(formatter.format(durationSeconds));
263    
264            return sb.toString();
265        }
266    
267        /**
268         * Returns the asset size in number of bytes.
269         * 
270         * @return the asset size in number of bytes
271         */
272        public long getSize() {
273            return size;
274        }
275    
276        /**
277         * Returns the asset size, formatted as a String.
278         * 
279         * @return the asset size, formatted as a String
280         */
281        public String getSizeFormatted() {
282            return MCRFilesystemNode.getSizeFormatted(size);
283        }
284    
285        /**
286         * Returns the ID of the content type of this asset
287         * 
288         * @return the ID of the content type of this asset
289         */
290        public String getContentTypeID() {
291            return contentTypeID;
292        }
293    
294        /**
295         * Returns the URL where clients can download a player for this asset
296         * 
297         * @return the URL where clients can download a player for this asset
298         */
299        public String getPlayerDownloadURL() {
300            return playerDownloadURL;
301        }
302    
303        /**
304         * Writes a metafile that starts a streaming player for this asset to an
305         * OutputStream, e. g. a ServletOutputStream. The browser then streams the
306         * asset. The client may provide a start and stop position to play only a
307         * certain part of the asset.
308         * 
309         * @param out
310         *            the OutputStream to write the player starter to
311         * @param startPos
312         *            the optional start position in the format "hh:mm:ss"
313         * @param stopPos
314         *            the optional stop position in the format "hh:mm:ss"
315         */
316        public abstract void getPlayerStarterTo(OutputStream out, String startPos, String stopPos) throws MCRPersistenceException;
317    
318        /**
319         * Returns the MIME type a servlet has to set in the HTTP response that
320         * delivers the player starter metafile to the browser
321         * 
322         * @return the MIME type of the player starter metafile
323         */
324        public String getPlayerStarterContentType() {
325            return playerStarterCT;
326        }
327    
328        /**
329         * Returns a string representation of this extender's data, useful for
330         * debugging.
331         * 
332         * @return a String containing useful information about this extender's data
333         */
334        public String toString() {
335            StringBuffer sb = new StringBuffer();
336            sb.append("Media Type      : ");
337            sb.append(isVideo() ? "Video\n" : "Audio\n");
338            sb.append("Bitrate         : ").append(getBitRateFormatted()).append("/sec.");
339            sb.append(" (").append(getBitRate()).append(")\n");
340    
341            if (isVideo()) {
342                sb.append("Framerate       : ").append(getFrameRateFormatted()).append(" fps");
343                sb.append(" (").append(getFrameRate()).append(")\n");
344            }
345    
346            sb.append("Duration        : ").append(getDurationFormatted("h", "min", "sec"));
347            sb.append(" (").append(getDurationTimecode()).append(")\n");
348            sb.append("Size            : ").append(getSizeFormatted());
349            sb.append(" (").append(getSize()).append(")\n");
350            sb.append("Content Type    : ").append(getContentTypeID()).append("\n");
351            sb.append("Player Download : ").append(playerDownloadURL);
352    
353            return sb.toString();
354        }
355    
356        /**
357         * Helper method to get a substring that lies between a prefix and a suffix
358         * string. If either prefix or suffix are not found in the string, the
359         * defaultValue is returned. This helper method is used by subclasses to
360         * parse metadata that is read from the server.
361         * 
362         * @param prefix
363         *            the string before the substring
364         * @param suffix
365         *            the string after the substring
366         * @param data
367         *            the string to search through
368         * @param defaultValue
369         *            the default to return when no match is found
370         * @return the substring between prefix and suffix
371         */
372        protected String getBetween(String prefix, String suffix, String data, String defaultValue) {
373            int from = data.indexOf(prefix);
374    
375            if (from == -1) {
376                return defaultValue;
377            }
378    
379            from += prefix.length();
380    
381            int to = data.indexOf(suffix, from);
382    
383            if (to == -1) {
384                return defaultValue;
385            }
386    
387            return data.substring(from, to).trim();
388        }
389    
390        /**
391         * Helper method that reads all data from an URLConnection input stream and
392         * forwards it to the given output stream.
393         * 
394         * @param connection
395         *            the URLConnection to get the InputStream from
396         * @param out
397         *            the OutputStream to write the bytes to
398         */
399        protected void forwardData(URLConnection connection, OutputStream out) throws IOException {
400            InputStream in = connection.getInputStream();
401            MCRUtils.copyStream(in, out);
402            out.close();
403        }
404    
405        /**
406         * Helper method that creates a URLConnection to a given URL and wraps
407         * possible IOException or MalformedURLExceptions
408         * 
409         * @param url
410         *            the URL to connect to
411         */
412        protected URLConnection getConnection(String url) throws MCRPersistenceException {
413            try {
414                return new URL(url).openConnection();
415            } catch (MalformedURLException exc) {
416                String msg = "Malformed Audio/Video Store URL: " + url;
417                throw new MCRConfigurationException(msg, exc);
418            } catch (IOException exc) {
419                String msg = "Could not get connection to Audio/Video Store URL: " + url;
420                throw new MCRPersistenceException(msg, exc);
421            }
422        }
423    
424        /**
425         * Helper method that connects to the given URL and returns the response as
426         * a String
427         * 
428         * @param url
429         *            the URL to connect to
430         * @return the response content as a String
431         */
432        protected String getMetadata(String url) throws MCRPersistenceException {
433            try {
434                URLConnection connection = getConnection(url);
435                connection.setConnectTimeout(getConnectTimeout());
436                ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
437                forwardData(connection, out);
438    
439                return new String(out.toByteArray());
440            } catch (IOException exc) {
441                String msg = "Could not get metadata from Audio/Video Store URL: " + url;
442                throw new MCRPersistenceException(msg, exc);
443            }
444        }
445    
446        protected int getConnectTimeout() {
447            return MCRConfiguration.instance().getInt("MCR.IFS.AVExtender.ConnectTimeout", 0);
448        }
449    }