├── .classpath
├── .gitignore
├── .project
├── AndroidManifest.xml
├── LICENSE
├── README.md
├── SubjectivePlayer.apk
├── SubjectivePlayer.iml
├── create_playlists_mobile.py
├── gen
└── org
│ └── univie
│ └── subjectiveplayer
│ ├── BuildConfig.java
│ ├── Manifest.java
│ └── R.java
├── project.properties
├── res
├── drawable-hdpi
│ ├── ic_menu_close_clear_cancel.png
│ ├── ic_menu_info_details.png
│ ├── ic_menu_preferences.png
│ └── icon.png
├── drawable-ldpi
│ ├── ic_menu_close_clear_cancel.png
│ ├── ic_menu_info_details.png
│ ├── ic_menu_preferences.png
│ └── icon.png
├── drawable-mdpi
│ ├── ic_menu_close_clear_cancel.png
│ ├── ic_menu_info_details.png
│ ├── ic_menu_preferences.png
│ └── icon.png
├── layout
│ ├── dialog_acr_custom.xml
│ ├── dialog_continuous_view.xml
│ ├── dialog_continuous_view_no_ticks.xml
│ ├── main.xml
│ ├── video_view.xml
│ └── video_view_overlay.xml
├── menu
│ └── subjective_menu.xml
├── values-de
│ └── strings.xml
├── values-en
│ └── strings.xml
├── values
│ ├── strings.xml
│ └── styles.xml
└── xml
│ └── preferences.xml
└── src
└── org
└── univie
└── subjectiveplayer
├── Configuration.java
├── CustomDialog.java
├── Logger.java
├── Methods.java
├── Session.java
├── SubjectivePlayer.java
├── SubjectivePlayerPreferences.java
└── SubjectivePlayerSession.java
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | .DS_Store
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | SubjectivePlayer
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The GNU General Public License v3
2 |
3 | SubjectivePlayer for Android
4 | Copyright (c) 2012-2017 Werner Robitza
5 |
6 | SubjectivePlayer for Android is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | SubjectivePlayer for Android is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with SubjectivePlayer for Android. If not, see .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Author: Werner Robitza
4 |
5 | ## Introduction
6 |
7 | Thanks for using SubjectivePlayer for Android. I built this software because there was nothing like it available yet and we definitely needed a video player capable of recording user opinion.
8 |
9 | There are quite a few tools around that exist for traditional PCs, mostly Windows-based, which show videos or still images to test persons and ask them to rate the respective video or image quality. Such tools adhere to the ITU recommendations on subjective evaluation of audio and video and have been used for a long time. The hardware that should be used is described precisely, and so are the testing procedures.
10 |
11 | Because the context of usage in mobile multimedia is totally different from the good old desktop PC at home, these ITU test recommendations do not really fit perfectly. We tried to come up with a simple and easy solution to present videos on an Android device and record the subjective opinion of viewers. Of course, when possible, ITU recommendations were followed or reinterpreted for mobile usage.
12 |
13 | ----
14 |
15 | ## Features and Non-Features
16 |
17 | To put it in a brief list, SubjectivePlayer for Android can:
18 |
19 | * play an indefinitely long list of MPEG-4 AVC (H.264) coded videos, either in .mp4 or .3gpp format
20 | * after each video ask the user for their opinion using a certain methodology
21 | * record a user ID so as to identify the test person later
22 | * log the user opinion to text files
23 |
24 | ----
25 |
26 | ## Requirements
27 |
28 | In order to use SubjectivePlayer for Android, you need to:
29 |
30 | * get a mobile phone running Android (tested with Galaxy S5 – use the "legacy" branch for older phones)
31 | * equip the phone with a SD card
32 | * activate the Debugging mode on the phone so you can use unsigned apps
33 | * preferably: a computer with the appropriate Android SDK installed to make your own changes
34 | * videos :)
35 |
36 | ## Installation
37 |
38 | Download the `SubjectivePlayer.apk` file in this repository. It's the newest build. You can point your mobile device to this file directly, and it will prompt you to install the software.
39 |
40 | Make sure you allow the application to access your SD card.
41 |
42 | ## Usage
43 |
44 | The application loads videos from the SD card of the device. If the device does not have a physical SD card, Android will simulate that there is one.
45 |
46 | Here is what you have to do to perform a test:
47 |
48 | - Prepare your movies as H.264/AAC video and audio, multiplexed in MP4. That ensures compatibility with the device.
49 | - Prepare a playlist, which consists of a list of video names, and call it `playlist1.cfg`, for example.
50 | - For each participant that you want to test, you need a new playlist file with their ID, so for example, you need to create the files `playlist1.cfg` and `playlist2.cfg` for two users.
51 |
52 | You can generate playlists using the `create_playlists_mobile.py` script.
53 |
54 | Now let's put everything on the device:
55 |
56 | ### If you have an SD card
57 |
58 | - Start the app
59 | - Go to options (hold the option button) and enable SD card
60 | - Quit and start the app again
61 | - Connect the phone and you will see three folders in `Android/data/org.univie.subjectiveplayer/files`: `SubjectiveMovies`, `SubjectiveCfg` and `SubjectiveLogs`.
62 | - Place your video files in the `SubjectiveMovies` folder.
63 | - Place your playlist in the `SubjectiveCfg` folder.
64 |
65 | ### If you have no SD card
66 |
67 | - Start the app once
68 | - Connect the phone and you will see three folders on the internal memory: `SubjectiveMovies`, `SubjectiveCfg` and `SubjectiveLogs`.
69 | - Place your video files in the `SubjectiveMovies` folder.
70 | - Place your playlist in the `SubjectiveCfg` folder.
71 |
72 | If you cannot write to these folders, you need to [use adb](http://lifehacker.com/the-easiest-way-to-install-androids-adb-and-fastboot-to-1586992378) and run something similar to this:
73 |
74 | adb push local-file /mnt/sdcard/SubjectiveMovies/
75 |
76 | ## Run the Test
77 |
78 | Now, when you start the app again, you can select the playlist by having the user enter their respective ID.
79 |
80 | After a test, the results are stored in the `SubjectiveLogs` folder. Each file corresponds to one user's test. The files contain the running number of video, the video name, and the corresponding rating as an integer number.
81 |
82 | ## License
83 |
84 | The GNU General Public License v3
85 |
86 | SubjectivePlayer for Android
87 | Copyright (c) 2012-2017 Werner Robitza
88 |
89 | SubjectivePlayer for Android is free software: you can redistribute it and/or modify
90 | it under the terms of the GNU General Public License as published by
91 | the Free Software Foundation, either version 3 of the License, or
92 | (at your option) any later version.
93 |
94 | SubjectivePlayer for Android is distributed in the hope that it will be useful,
95 | but WITHOUT ANY WARRANTY; without even the implied warranty of
96 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
97 | GNU General Public License for more details.
98 |
99 | You should have received a copy of the GNU General Public License
100 | along with SubjectivePlayer for Android. If not, see .
101 |
--------------------------------------------------------------------------------
/SubjectivePlayer.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/SubjectivePlayer.apk
--------------------------------------------------------------------------------
/SubjectivePlayer.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/create_playlists_mobile.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Generate mobile playlists for SubjectivePlayer
3 | # Author: Werner Robitza
4 |
5 | import os
6 | import argparse
7 | import glob
8 | import re
9 | import math
10 | import pprint
11 | import random
12 |
13 | TRAINING_SET = [
14 | "01_tr03_SRC200_HRC03.mp4",
15 | "02_tr03_SRC414_HRC61.mp4",
16 | "03_tr03_SRC118_HRC68.mp4",
17 | "04_tr03_SRC115_HRC76.mp4",
18 | "05_tr03_SRC305_HRC69.mp4",
19 | "06_tr03_SRC218_HRC02.mp4",
20 | "07_TR00_SRC00_HRC01.mp4"
21 | ]
22 |
23 | SUFFIX = ".cfg"
24 |
25 | parser = argparse.ArgumentParser(
26 | description='''This program generates mobile playlists for a number of users in a random fashion''')
27 | parser.add_argument('-i', '--input', type = str, default="./avbuffpvs/", help = '''Path to PVS''')
28 | parser.add_argument('-o', '--output', type = str, default="./playlists_mobile/", help = '''Path to output dir''')
29 | parser.add_argument('-n', '--number', type = int, help = '''Number of playlists to generate''', default = 30)
30 | parser.add_argument('-s', '--sessions', type = int, help = "Number of sessions", default = 3)
31 | parser.add_argument('-p', '--prime', default=False, action="store_true",
32 | help="Use prime numbers for ID generation")
33 |
34 | args = parser.parse_args()
35 |
36 | # ---------------------------------------------------------------------------------------------------------
37 |
38 | # check if number is prime
39 | def is_prime(n):
40 | for i in range(2, int(math.sqrt(n)) + 1):
41 | if n % i == 0:
42 | return False
43 | return True
44 |
45 | # get a number of primes starting with a number
46 | def get_primes(n, init = 0):
47 | primes = list()
48 | i = init
49 | while(len(primes) < n):
50 | if(is_prime(i)):
51 | primes.append(i)
52 | i = i + 1
53 | return primes
54 |
55 |
56 | # split a list into equal parts
57 | def get_randomized_and_split(input_list, n_per_split):
58 | shuffled = sorted(input_list, key = lambda k: random.random())
59 | l = [ shuffled [i:i + n_per_split] for i in range(0, (args.sessions-1)*n_per_split, n_per_split)]
60 | l.append(shuffled[(args.sessions-1)*n_per_split:])
61 | return l
62 |
63 | # create test list
64 | pvs = [ os.path.basename(f) for f in glob.glob(args.input + "*.mp4") ]
65 |
66 | # remove training videos
67 | pvs = [ p for p in pvs if p not in TRAINING_SET ]
68 |
69 | # split into sessions
70 | pvs_per_session = int(len(pvs) / args.sessions)
71 |
72 | # get prime numbers for ID generation
73 | primes = get_primes(args.number, init = 3)
74 |
75 | # create output dir if necessary
76 | if not os.path.exists(args.output):
77 | os.makedirs(args.output)
78 |
79 | # generate playlists and write to files
80 | for user_id, prime in enumerate(primes):
81 | # start with ID 1 for users
82 | user_id = user_id + 1
83 | playlists = get_randomized_and_split(pvs, pvs_per_session)
84 |
85 | # IDs for files
86 | training_id = str(user_id) + "0"
87 | session_ids = list()
88 |
89 | # session 1, 2, 3
90 | for i in range(1, args.sessions + 1):
91 | if args.prime:
92 | session_ids.append(str(prime ** i))
93 | else:
94 | session_ids.append(str(user_id) + str(i))
95 |
96 | training_file = open(args.output + 'playlist' + training_id + SUFFIX, 'w')
97 | print("[info] writing training file for user " + str(user_id) + ": " + training_file.name)
98 | for f in TRAINING_SET:
99 | training_file.write(f + "\n")
100 | training_file.close()
101 |
102 | for session_id, playlist in zip(session_ids, playlists):
103 | session_file = open(args.output + 'playlist' + session_id + SUFFIX, 'w')
104 | print("[info] writing session file for user " + str(user_id) + ": " + session_file.name)
105 | for f in playlist:
106 | session_file.write(f + "\n")
107 | session_file.close()
108 |
--------------------------------------------------------------------------------
/gen/org/univie/subjectiveplayer/BuildConfig.java:
--------------------------------------------------------------------------------
1 | /*___Generated_by_IDEA___*/
2 |
3 | package org.univie.subjectiveplayer;
4 |
5 | /* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */
6 | public final class BuildConfig {
7 | public final static boolean DEBUG = Boolean.parseBoolean(null);
8 | }
--------------------------------------------------------------------------------
/gen/org/univie/subjectiveplayer/Manifest.java:
--------------------------------------------------------------------------------
1 | /*___Generated_by_IDEA___*/
2 |
3 | package org.univie.subjectiveplayer;
4 |
5 | /* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */
6 | public final class Manifest {
7 | }
--------------------------------------------------------------------------------
/gen/org/univie/subjectiveplayer/R.java:
--------------------------------------------------------------------------------
1 | /* AUTO-GENERATED FILE. DO NOT MODIFY.
2 | *
3 | * This class was automatically generated by the
4 | * aapt tool from the resource data it found. It
5 | * should not be modified by hand.
6 | */
7 |
8 | package org.univie.subjectiveplayer;
9 |
10 | public final class R {
11 | public static final class attr {
12 | }
13 | public static final class drawable {
14 | public static final int ic_menu_close_clear_cancel=0x7f020000;
15 | public static final int ic_menu_info_details=0x7f020001;
16 | public static final int ic_menu_preferences=0x7f020002;
17 | public static final int icon=0x7f020003;
18 | }
19 | public static final class id {
20 | public static final int button_filebrowser=0x7f070015;
21 | public static final int button_methodbrowser=0x7f070018;
22 | public static final int button_start=0x7f07001b;
23 | public static final int dialog_continuous_label0=0x7f070002;
24 | public static final int dialog_continuous_label1=0x7f070003;
25 | public static final int dialog_continuous_label10=0x7f07000c;
26 | public static final int dialog_continuous_label2=0x7f070004;
27 | public static final int dialog_continuous_label3=0x7f070005;
28 | public static final int dialog_continuous_label4=0x7f070006;
29 | public static final int dialog_continuous_label5=0x7f070007;
30 | public static final int dialog_continuous_label6=0x7f070008;
31 | public static final int dialog_continuous_label7=0x7f070009;
32 | public static final int dialog_continuous_label8=0x7f07000a;
33 | public static final int dialog_continuous_label9=0x7f07000b;
34 | public static final int dialog_continuous_no_ticks_label_max=0x7f070011;
35 | public static final int dialog_continuous_no_ticks_label_min=0x7f070010;
36 | public static final int dialog_continuous_no_ticks_tablerow1=0x7f07000f;
37 | public static final int dialog_continuous_seekbar=0x7f07000d;
38 | public static final int dialog_continuous_tablelayout=0x7f070000;
39 | public static final int dialog_continuous_tablelayout_no_ticks=0x7f07000e;
40 | public static final int dialog_continuous_tablerow1=0x7f070001;
41 | public static final int edit_id=0x7f07001a;
42 | public static final int menu_about=0x7f07001e;
43 | public static final int menu_exit=0x7f07001f;
44 | public static final int menu_preferences=0x7f07001d;
45 | public static final int scroll_view=0x7f070012;
46 | public static final int text_select_config=0x7f070013;
47 | public static final int text_select_config_selected=0x7f070014;
48 | public static final int text_select_id=0x7f070019;
49 | public static final int text_select_method=0x7f070016;
50 | public static final int text_select_method_selected=0x7f070017;
51 | public static final int video_surface=0x7f07001c;
52 | }
53 | public static final class layout {
54 | public static final int dialog_continuous_view=0x7f030000;
55 | public static final int dialog_continuous_view_no_ticks=0x7f030001;
56 | public static final int main=0x7f030002;
57 | public static final int video_view=0x7f030003;
58 | public static final int video_view_overlay=0x7f030004;
59 | }
60 | public static final class menu {
61 | public static final int subjective_menu=0x7f060000;
62 | }
63 | public static final class string {
64 | public static final int about_body=0x7f050010;
65 | public static final int about_caption=0x7f05000f;
66 | public static final int app_name=0x7f050001;
67 | public static final int dialog_no_ticks_max=0x7f050019;
68 | public static final int dialog_no_ticks_min=0x7f05001a;
69 | public static final int error_nodata_body=0x7f05000d;
70 | public static final int error_nodata_caption=0x7f05000e;
71 | public static final int hello=0x7f050000;
72 | public static final int menu_about=0x7f050004;
73 | public static final int menu_exit=0x7f050005;
74 | public static final int menu_preferences=0x7f050009;
75 | public static final int preferences_acrnumbers=0x7f050015;
76 | public static final int preferences_acrnumbers_summary_false=0x7f050017;
77 | public static final int preferences_acrnumbers_summary_true=0x7f050016;
78 | public static final int preferences_categories_acr=0x7f050014;
79 | public static final int preferences_categories_config=0x7f05000c;
80 | public static final int preferences_categories_continuous=0x7f05001b;
81 | public static final int preferences_configsuffix=0x7f05000a;
82 | public static final int preferences_configsuffix_summary=0x7f05000b;
83 | public static final int preferences_noticks_caption=0x7f05001c;
84 | public static final int preferences_noticks_summary_false=0x7f05001e;
85 | public static final int preferences_noticks_summary_true=0x7f05001d;
86 | public static final int rate_ACR_caption=0x7f050013;
87 | public static final int rate_continuous_caption=0x7f050018;
88 | public static final int select_config=0x7f050002;
89 | public static final int select_config_selected=0x7f050011;
90 | public static final int select_filebrowser=0x7f050006;
91 | public static final int select_id=0x7f050003;
92 | public static final int select_method=0x7f050007;
93 | public static final int select_method_browser=0x7f050008;
94 | public static final int select_method_selected=0x7f050012;
95 | }
96 | public static final class xml {
97 | public static final int preferences=0x7f040000;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system use,
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 |
10 | # Project target.
11 | target=android-4
12 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_menu_close_clear_cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-hdpi/ic_menu_close_clear_cancel.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-hdpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_menu_preferences.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-hdpi/ic_menu_preferences.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-hdpi/icon.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_menu_close_clear_cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-ldpi/ic_menu_close_clear_cancel.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-ldpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_menu_preferences.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-ldpi/ic_menu_preferences.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-ldpi/icon.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_menu_close_clear_cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-mdpi/ic_menu_close_clear_cancel.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-mdpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_menu_preferences.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-mdpi/ic_menu_preferences.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slhck/SubjectivePlayer/599ab8de11dec791ce6a36892baedae17c3e4f99/res/drawable-mdpi/icon.png
--------------------------------------------------------------------------------
/res/layout/dialog_acr_custom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
18 |
19 |
27 |
28 |
31 |
32 |
40 |
41 |
49 |
50 |
51 |
52 |
55 |
56 |
64 |
65 |
72 |
73 |
74 |
77 |
78 |
86 |
87 |
94 |
95 |
96 |
99 |
100 |
108 |
109 |
116 |
117 |
118 |
119 |
122 |
123 |
131 |
132 |
139 |
140 |
141 |
142 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/res/layout/dialog_continuous_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
13 |
16 |
19 |
22 |
25 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |
47 |
48 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/res/layout/dialog_continuous_view_no_ticks.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
13 |
16 |
17 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
25 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/res/layout/video_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/res/layout/video_view_overlay.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/res/menu/subjective_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Willkommen im SubjectivePlayer für Android!
4 | SubjectivePlayer
5 | Bitte Konfigurationsdatei mit Videos auswählen!
6 | Bitte geben Sie die Teilnehmer-ID ein!
7 | Über
8 | Beenden
9 | Konfigurationsdatei wählen
10 |
11 | Bitte wählen Sie eine Testmethode!
12 | Methode wählen
13 |
14 |
15 | Einstellungen
16 |
17 | Suffix der Konfig-Datei
18 | Bearbeiten Sie den Suffix der Konfig-Datei
19 | Konfigurationsdateien
20 | ID oder Konfig-Datei zu ID fehlt!
21 | Daten fehlen
22 | Diese ID wurde bereits verwendet!
23 | ID schon verwendet
24 | Über SubjectivePlayer
25 | SubjectivePlayer, geschrieben von Werner Robitza für das Projekt CACMTV, Universität Wien. code.google.com/p/subjectiveplayer für Details!
26 | Ausgewählte Konfiguration:
27 | Ausgewählte Methode:
28 | Bitte bewerten
29 | Bitte bewerten Sie die audiovisuelle Qualität!
30 | ACR Einstellungen
31 | ACR Zahlen speichern
32 | Speichere Zahlen anstatt Kategorien in Log-Datei
33 | Speichere Kategorien anstatt Zahlen in Log-Datei
34 | SD Card verwenden
35 | Verwende SD card
36 | Verwende SD card nicht
37 | Mehrfache IDs
38 | Erlaube mehrfache Verwendung von IDs
39 | Erlaube keine mehrfache Verwendung von IDs
40 | Bitte bewerten
41 | Max
42 | Min
43 | Kontinuierliche Bewertung
44 | Keine Markierungen
45 | Keine Markierungen am Slider angezeigt
46 | Markierungen am Slider werden angezeigt
47 |
48 |
49 | Ausgezeichnet
50 | Gut
51 | Ordentlich
52 | Dürftig
53 | Schlecht
54 |
55 |
56 |
57 | Ausgezeichnet
58 | Gut
59 | Ordentlich
60 | Dürftig
61 | Schlecht
62 |
63 |
64 |
--------------------------------------------------------------------------------
/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Welcome to SubjectivePlayer for Android!
4 | SubjectivePlayer
5 | Please select a config file containing the videos!
6 | Please enter your participant ID!
7 | About
8 | Exit
9 | Select a config file
10 |
11 | Please select a testing method!
12 | Select a method
13 |
14 |
15 | Preferences
16 |
17 | Config file suffix
18 | Edit the suffix for configuration files
19 | Configuration files
20 | Missing config file for this ID, or no ID given!
21 | Missing data
22 | This ID has already been used!
23 | ID already used
24 | About SubjectivePlayer
25 | SubjectivePlayer for Android is written by Werner Robitza in the project CACMTV, University of Vienna. Please go to code.google.com/p/subjectiveplayer for help! Thanks a lot for using!
26 | Selected config:
27 | Selected method:
28 | Please rate
29 | Please rate the audiovisual quality!
30 | ACR preferences
31 | Log ACR numbers
32 | Write numbers instead of category names in log file
33 | Write category names instead of numbers in log file
34 | Use SD Card
35 | Use SD card as storage if inserted
36 | Do not use SD card as storage
37 | Allow duplicate IDs
38 | Allow duplicate IDs in the test
39 | Do not allow duplicate IDs in the test
40 | Please rate
41 | Max
42 | Min
43 | Continuous rating preferences
44 | No ticks
45 | Show no ticks on the slider
46 | Show ticks on the slider
47 |
48 |
49 | Excellent
50 | Good
51 | Fair
52 | Poor
53 | Bad
54 |
55 |
56 |
57 | Excellent
58 | Good
59 | Fair
60 | Poor
61 | Bad
62 |
63 |
64 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Welcome to SubjectivePlayer for Android!
4 | SubjectivePlayer
5 | Please select a config file containing the videos!
6 | Please enter your participant ID!
7 | About
8 | Exit
9 | Select a config file
10 |
11 | Please select a testing method!
12 | Select a method
13 |
14 |
15 | Preferences
16 |
17 | Config file suffix
18 | Edit the suffix for configuration files
19 | Configuration files
20 | Missing config file, method or ID. Please enter all data!
21 | Missing data
22 | This ID has already been used!
23 | ID already used
24 | About SubjectivePlayer
25 | SubjectivePlayer for Android is written by Werner Robitza in the project CACMTV, University of Vienna. Please go to code.google.com/p/subjectiveplayer for help! Thanks a lot for using!
26 | Selected config:
27 | Selected method:
28 | Please rate
29 | Please rate the audiovisual quality!
30 | ACR preferences
31 | Log ACR numbers
32 | Write numbers instead of category names in log file
33 | Write category names instead of numbers in log file
34 | Use SD Card
35 | Use SD card as storage if inserted
36 | Do not use SD card as storage
37 | Allow duplicate IDs
38 | Allow duplicate IDs in the test
39 | Do not allow duplicate IDs in the test
40 | Please rate
41 | Max
42 | Min
43 | Continuous rating preferences
44 | No ticks
45 | Show no ticks on the slider
46 | Show ticks on the slider
47 |
48 |
49 | Excellent
50 | Good
51 | Fair
52 | Poor
53 | Bad
54 |
55 |
56 |
57 | Excellent
58 | Good
59 | Fair
60 | Poor
61 | Bad
62 |
63 |
64 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
12 |
18 |
24 |
25 |
28 |
34 |
35 |
38 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/Configuration.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | import java.io.File;
20 | import java.util.Collections;
21 | import java.util.HashSet;
22 | import java.util.Set;
23 | import java.util.regex.Pattern;
24 |
25 | import android.content.Context;
26 | import android.content.SharedPreferences;
27 | import android.os.Build;
28 | import android.os.Environment;
29 | import android.text.TextUtils;
30 | import android.util.Log;
31 |
32 | public abstract class Configuration {
33 |
34 | private static final String TAG = Configuration.class.getSimpleName();
35 |
36 | private static Context sCtx = null;
37 |
38 | /** File handle for the root of the SD card */
39 | public static File sStorage = null;
40 |
41 | /** File handle for the root of the app data folder */
42 | public static File sFolderApproot = null;
43 |
44 | /** File handle for the folder in which videos are stored */
45 | public static File sFolderVideos = null;
46 |
47 | /** File handle for the folder in which log files are stored */
48 | public static File sFolderLogs = null;
49 |
50 | /** File handle for a configuration file */
51 | public static File sFileConfig = null;
52 |
53 | /** File name of a configuration file */
54 | public static String sFileConfigName = null;
55 |
56 | /**
57 | * Path of the folder in which videos are stored, relative to the SD card
58 | * root with a trailing slash. If it does not exist, it will be created
59 | * automatically on the SD card.
60 | */
61 | public static final String PATH_VIDEOS = new String("SubjectiveMovies/");
62 |
63 | /**
64 | * Path of the folder in which log files are stored, relative to the SD card
65 | * root with a trailing slash. If it does not exist, it will be created
66 | * automatically on the SD card.
67 | */
68 | public static final String PATH_LOGS = new String("SubjectiveLogs/");
69 |
70 | /**
71 | * Path of the folder in which config files are stored, relative to the SD card
72 | * root with a trailing slash. If it does not exist, it will be created
73 | * automatically on the SD card.
74 | */
75 | public static final String PATH_CONFIG = new String("SubjectiveCfg/");
76 |
77 | /**
78 | * The suffix of configuration files. It can be set in the application
79 | * preferences
80 | */
81 | public static String sConfigSuffix = null;
82 |
83 | /**
84 | * Controls whether the logger writes the numbers instead of "Excellent, .."
85 | * labels for ACR
86 | */
87 | public static boolean sAcrNumbers = true;
88 |
89 | /**
90 | * Controls whether the continuous dialog shows no ticks and just a maximum
91 | * and minimum label
92 | */
93 | public static boolean sNoTicks = false;
94 |
95 |
96 | /**
97 | * Controls whether the SD card is used
98 | */
99 | public static boolean sUseSdCard = false;
100 |
101 |
102 | /**
103 | * Allow using same ID twice or more
104 | */
105 | public static boolean sAllowDuplicateIds = false;
106 |
107 | /** The container for all application preferences */
108 | private static SharedPreferences sPreferences = null;
109 |
110 |
111 | /**
112 | * Internally updates the preference values, called by setPreferences on
113 | * each activity resume. This method only overwrites the internal members of
114 | * this class with the contents of the SharedPreferences.
115 | */
116 | private static void updatePreferences() {
117 | if (sPreferences != null) {
118 | sConfigSuffix = sPreferences.getString("configsuffix", "cfg");
119 | sAcrNumbers = sPreferences.getBoolean("acrnumbers", true);
120 | sNoTicks = sPreferences.getBoolean("noticks", false);
121 | sUseSdCard = sPreferences.getBoolean("usesdcard", false);
122 | sAllowDuplicateIds = sPreferences.getBoolean("allowduplicateids", false);
123 | }
124 | }
125 |
126 | /**
127 | * Sets the preferences and updates them internally. This method should be
128 | * called by an activity in the onResume method.
129 | */
130 | public static void setPreferences(SharedPreferences preferences) {
131 | Configuration.sPreferences = preferences;
132 | updatePreferences();
133 | }
134 |
135 | /**
136 | * Creates a directory
137 | * @param dir directory
138 | */
139 | private static void createOrCheckDir(File dir) throws Exception {
140 | if (dir.exists()) return;
141 | boolean created = dir.mkdirs();
142 | if (!created) throw new Exception("Couldn't create " + dir);
143 |
144 | }
145 |
146 | /**
147 | * Tries to initialize the SD card, obtain the file handles and then create
148 | * folders if they don't exist already.
149 | */
150 | public static void initialize(Context ctx) throws Exception {
151 | sCtx = ctx;
152 | if (Environment.getExternalStorageState().equalsIgnoreCase(Environment.MEDIA_MOUNTED)) {
153 | // refers to the internal storage
154 | sStorage = Environment.getExternalStorageDirectory();
155 | Log.d(TAG, "Using internal storage at " + sStorage.toString());
156 | } else {
157 | Log.e(TAG, "Could not initialize internal storage");
158 | throw new Exception("Could not open internal storage!");
159 | }
160 |
161 | // check if the user requested SD card instead
162 | if (sUseSdCard) {
163 | File[] extDirs = ctx.getExternalFilesDirs(null);
164 | File extSdPath = new File("");
165 | for (File f : extDirs) {
166 | if (f.toString().contains("extSdCard")) {
167 | extSdPath = f;
168 | }
169 | }
170 | if (extSdPath.exists() && extSdPath.canWrite()) {
171 | sStorage = extSdPath;
172 | Log.d(TAG, "Using external SD card at " + sStorage.toString());
173 | } else {
174 | // do nothing; use internal storage instead
175 | }
176 | }
177 |
178 | sFolderApproot = new File(sStorage, PATH_CONFIG);
179 | sFolderVideos = new File(sStorage, PATH_VIDEOS);
180 | sFolderLogs = new File(sStorage, PATH_LOGS);
181 |
182 | // create the data and video directory if they don't exist already
183 | Configuration.createOrCheckDir(sFolderApproot);
184 | Configuration.createOrCheckDir(sFolderVideos);
185 | Configuration.createOrCheckDir(sFolderLogs);
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/CustomDialog.java:
--------------------------------------------------------------------------------
1 | package org.univie.subjectiveplayer;
2 |
3 | import android.app.Dialog;
4 | import android.content.Context;
5 |
6 | public class CustomDialog extends Dialog {
7 | protected CustomDialog (Context context) {
8 | super(context, R.style.AlertDialogTheme);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/Logger.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | import java.io.BufferedWriter;
20 | import java.io.File;
21 | import java.io.FileWriter;
22 | import java.io.IOException;
23 | import java.text.SimpleDateFormat;
24 | import java.util.Date;
25 |
26 | /**
27 | * Logging Class that writes log files with the user's ratings
28 | */
29 | public abstract class Logger {
30 |
31 | /** File handle for the log file */
32 | private static File sLogFile = null;
33 | /** The constructed current file name */
34 | private static String sFileName = null;
35 | /**
36 | * The date format as specified in SimpleDateFormat for writing the filename
37 | */
38 | private static final String DATE_FORMAT = "yyyyMMdd-HHmm";
39 | /** The CSV separator character */
40 | private static final char SEP_CSV = ',';
41 | /** The File separator character, e.g. a space */
42 | private static final char SEP_FILE = '_';
43 | /** Whether a CSV header should be written or not */
44 | private static final boolean HEADER = true;
45 | /** The file suffix */
46 | private static final String SUFFIX = "txt";
47 |
48 | /** Whether a continuous log is running or not */
49 | private static boolean sContinuousLogStarted = false;
50 |
51 | private static FileWriter sFileWriter;
52 | private static BufferedWriter sBufferedWriter;
53 |
54 | /**
55 | * Check whether an ID already exists in the log files
56 | */
57 | public static boolean idExists(int id) {
58 |
59 | for (File f : Configuration.sFolderLogs.listFiles()) {
60 | String idPart = f.getName().split("_", 2)[0];
61 | try {
62 | int currentId = Integer.parseInt(idPart);
63 | if (currentId == id) {
64 | return true;
65 | }
66 | } catch (Exception e) {
67 | // do nothing
68 | }
69 | }
70 | return false;
71 | }
72 |
73 | /**
74 | * Writes a log of the session data to the specified file
75 | */
76 | public static void writeSessionLogCSV() {
77 | try {
78 | SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
79 | String methodName = Methods.METHOD_NAMES[Session.sCurrentMethod];
80 | methodName = methodName.replace(' ', SEP_FILE);
81 |
82 | // ID_Date_Method.txt
83 | sFileName = "" + Session.sParticipantId + SEP_FILE
84 | + format.format(new Date()) + SEP_FILE + methodName
85 | + "." + SUFFIX;
86 | sLogFile = new File(Configuration.sFolderLogs, sFileName);
87 |
88 | sFileWriter = new FileWriter(sLogFile);
89 | sBufferedWriter = new BufferedWriter(sFileWriter);
90 |
91 | if (HEADER) {
92 | sBufferedWriter.write("VIDEO" + SEP_CSV + "VIDEONAME" + SEP_CSV
93 | + "RATING" + SEP_CSV + "RATINGTIME");
94 | sBufferedWriter.newLine();
95 | }
96 |
97 | // iterate through all ratings
98 | for (int i = 0; i < Session.sTracks.size(); ++i) {
99 |
100 | // write the first line
101 | sBufferedWriter.write("" + i + SEP_CSV + Session.sTracks.get(i)
102 | + SEP_CSV);
103 |
104 | // write the rating depending on the method
105 | switch (Session.sCurrentMethod) {
106 | case (Methods.TYPE_ACR_CATEGORICAL):
107 | Integer rating = Session.sRatings.get(i);
108 | // Use this for standard ACR dialog
109 | // Integer invertedRating = 5 - rating;
110 | if (Configuration.sAcrNumbers) {
111 | // sBufferedWriter.write(invertedRating.toString());
112 | sBufferedWriter.write(rating.toString());
113 | }
114 | else {
115 | sBufferedWriter
116 | .write(Methods.STATIC_LABELS_ACR[Session.sRatings
117 | .get(i)]);
118 | }
119 | break;
120 | case (Methods.TYPE_CONTINUOUS):
121 | sBufferedWriter.write(Session.sRatings.get(i).toString());
122 | break;
123 | default:
124 | sBufferedWriter.write(i);
125 | }
126 | sBufferedWriter.write(SEP_CSV);
127 | sBufferedWriter.write(Session.sRatingTime.get(i).toString());
128 | sBufferedWriter.newLine();
129 | }
130 |
131 | sBufferedWriter.close();
132 | sFileWriter.close();
133 | } catch (IOException e) {
134 | // TODO Auto-generated catch block
135 | e.printStackTrace();
136 | }
137 | }
138 |
139 | /**
140 | * Starts a continuous results file, including header information if needed
141 | *
142 | * @param videoName
143 | * The name of the video, to be used for the file name
144 | */
145 | public static void startContinuousLogCSV(String videoName) {
146 | try {
147 | SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
148 | String methodName = Methods.METHOD_NAMES[Session.sCurrentMethod];
149 | methodName = methodName.replace(' ', SEP_FILE);
150 |
151 | sFileName = "" + Session.sParticipantId + SEP_FILE
152 | + format.format(new Date()) + SEP_FILE + methodName
153 | + SEP_FILE + videoName + "." + SUFFIX;
154 | sLogFile = new File(Configuration.sFolderLogs, sFileName);
155 |
156 | sFileWriter = new FileWriter(sLogFile);
157 | sBufferedWriter = new BufferedWriter(sFileWriter);
158 |
159 | if (HEADER) {
160 | sBufferedWriter.write("USERID" + SEP_CSV + "VIDEONAME"
161 | + SEP_CSV + "TIMESTAMP" + SEP_CSV + "RATING");
162 | sBufferedWriter.newLine();
163 | }
164 |
165 | sContinuousLogStarted = true;
166 |
167 | } catch (Exception e) {
168 | // TODO Auto-generated catch block
169 | e.printStackTrace();
170 | }
171 | }
172 |
173 | /**
174 | * Writes one continuous data line to the file. IO issues here?
175 | *
176 | * @param videoName
177 | * The name of the video file itself
178 | * @param timeStamp
179 | * The date timestamp that should appear in the result file
180 | * @param data
181 | * The rating itself, whatever it is
182 | */
183 | public static void writeContinuousData(String videoName, String timeStamp,
184 | String data) {
185 | try {
186 | if (!sContinuousLogStarted) {
187 | throw new Exception("Can't log if file wasn't started yet.");
188 | }
189 |
190 | sBufferedWriter.write("" + Session.sParticipantId + SEP_CSV
191 | + videoName + SEP_CSV + timeStamp + SEP_CSV + data);
192 | sBufferedWriter.newLine();
193 | sBufferedWriter.flush();
194 | } catch (Exception e) {
195 | // TODO Auto-generated catch block
196 | e.printStackTrace();
197 | }
198 |
199 | }
200 |
201 | public static void closeContinuousLogCSV() {
202 | sContinuousLogStarted = false;
203 |
204 | try {
205 | sBufferedWriter.close();
206 | sFileWriter.close();
207 | } catch (IOException e) {
208 | // TODO Auto-generated catch block
209 | e.printStackTrace();
210 | }
211 | }
212 |
213 | }
214 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/Methods.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | /**
20 | * Class container for all implemented methods
21 | */
22 | public abstract class Methods {
23 |
24 | // method names
25 | public static final String[] METHOD_NAMES = { "ACR - Categorical",
26 | "Continuous scale", "DSIS", "Continuous rating" };
27 |
28 | // method IDs, these should be in the same order
29 | // as the names above!
30 | public static final int UNDEFINED = -1;
31 | public static final int TYPE_ACR_CATEGORICAL = 0;
32 | public static final int TYPE_CONTINUOUS = 1;
33 | public static final int TYPE_DSIS_CATEGORICAL = 2;
34 | public static final int TYPE_CONTINUOUS_RATING = 3;
35 |
36 | // labels for the ACR category
37 | public static final String[] STATIC_LABELS_ACR = {
38 | "Excellent",
39 | "Good",
40 | "Fair",
41 | "Poor",
42 | "Bad"
43 | };
44 |
45 | // labels for the impairment scale
46 | public static final String[] LABELS_DSIS = {
47 | "Imperceptible",
48 | "Perceptible, but not annoying",
49 | "Slightly Annoying",
50 | "Annoying",
51 | "Very Annoying"
52 | };
53 |
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/Session.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | import java.io.BufferedReader;
20 | import java.io.DataInputStream;
21 | import java.io.File;
22 | import java.io.FileInputStream;
23 | import java.io.InputStreamReader;
24 | import java.util.Vector;
25 |
26 | import android.util.Log;
27 |
28 | /**
29 | * Class that stores session data for each participant
30 | */
31 | public abstract class Session {
32 |
33 | private static final String TAG = Session.class.getSimpleName();
34 |
35 | /** the participant's ID */
36 | public static int sParticipantId = 0;
37 |
38 | /** the ID of the method the participant is using */
39 | public static int sCurrentMethod = Methods.UNDEFINED;
40 |
41 | /** the index of the currently playing track */
42 | public static int sCurrentTrack = 0;
43 |
44 | /** the tracks to be shown */
45 | public static Vector sTracks = new Vector();
46 |
47 | /** the ratings for each corresponding track */
48 | public static Vector sRatings = new Vector();
49 |
50 | /** the rating time for each corresponding track */
51 | public static Vector sRatingTime = new Vector();
52 |
53 |
54 |
55 | /**
56 | * Tries to extract all video file names from the configuration file passed
57 | * and saves them to a vector of Strings. These are just the names of the
58 | * videos, not their complete paths. The path can be generated by combining
59 | * it from the settings in the Configuration. Missing files will not be
60 | * included in the playlist.
61 | */
62 | public static void readVideosFromFile(File configFile) {
63 | try {
64 | if (configFile.exists() && configFile.canRead()) {
65 |
66 | // prepare readers
67 | FileInputStream fin = new FileInputStream(configFile);
68 | DataInputStream din = new DataInputStream(fin);
69 | InputStreamReader ir = new InputStreamReader(din);
70 | BufferedReader br = new BufferedReader(ir);
71 |
72 | // read from the file
73 | String currentLine = new String();
74 | while ((currentLine = br.readLine()) != null) {
75 | sTracks.add(currentLine);
76 | }
77 |
78 | // take care of files that can not be found, so we don't have to
79 | // check later (we should maybe, but yeah... TODO)
80 | for (int i = 0; i < sTracks.size(); i++) {
81 | File f = new File(Configuration.sFolderVideos, sTracks
82 | .get(i));
83 | if (!f.exists()) {
84 | sTracks.remove(i);
85 | }
86 | }
87 |
88 | // cleanup streams
89 | br.close();
90 | ir.close();
91 | din.close();
92 | fin.close();
93 | }
94 | } catch (Exception e) {
95 | Log.e(TAG, "Could not read Videos from file: " + e.getMessage());
96 | }
97 | }
98 |
99 | /**
100 | * Resets the session before another round.
101 | */
102 | public static void reset() {
103 | sParticipantId = 0;
104 | sCurrentTrack = 0;
105 | sCurrentMethod = Methods.UNDEFINED;
106 | sTracks = new Vector();
107 | sRatings = new Vector();
108 | sRatingTime = new Vector();
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/SubjectivePlayer.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | import java.io.File;
20 | import java.io.FilenameFilter;
21 |
22 | import android.app.Activity;
23 | import android.app.AlertDialog;
24 | import android.app.Dialog;
25 | import android.content.DialogInterface;
26 | import android.content.Intent;
27 | import android.os.Bundle;
28 | import android.preference.PreferenceManager;
29 | import android.text.Editable;
30 | import android.text.InputType;
31 | import android.util.Log;
32 | import android.view.Menu;
33 | import android.view.MenuInflater;
34 | import android.view.MenuItem;
35 | import android.view.MotionEvent;
36 | import android.view.View;
37 | import android.view.View.OnClickListener;
38 | import android.widget.Button;
39 | import android.widget.EditText;
40 | import android.widget.TextView;
41 |
42 | public class SubjectivePlayer extends Activity {
43 |
44 | private static final String TAG = SubjectivePlayer.class.getSimpleName();
45 |
46 | static final int DIALOG_ABOUT_ID = 0;
47 | static final int DIALOG_FILEBROWSER = 1;
48 | static final int DIALOG_METHODBROWSER = 2;
49 | static final int DIALOG_EMPTY = 3;
50 | static final int DIALOG_ID_ALREADY_USED = 4;
51 |
52 | private static EditText mEditId = null;
53 |
54 | /*
55 | * Main code
56 | */
57 | /**
58 | * Called when the activity is being started or restarted
59 | */
60 | @Override
61 | public void onResume() {
62 | super.onResume();
63 |
64 | // reload any updated preferences
65 | Configuration.setPreferences(PreferenceManager
66 | .getDefaultSharedPreferences(getBaseContext()));
67 |
68 |
69 | // reset the session
70 | Session.reset();
71 |
72 | mEditId.setText("");
73 | }
74 |
75 | /**
76 | * Called when the activity is first created.
77 | */
78 | @Override
79 | public void onCreate(Bundle savedInstanceState) {
80 | super.onCreate(savedInstanceState);
81 |
82 | setContentView(R.layout.main);
83 |
84 | // initialize the SD card for retrieving all config and media files
85 | try {
86 | Configuration.initialize(getApplicationContext());
87 | } catch (Exception e) {
88 | Log.e(TAG, "Error while initializing: " + e.getMessage());
89 | e.printStackTrace();
90 | // TODO do something useful, not just quit
91 | finish();
92 | }
93 |
94 | mEditId = (EditText) findViewById(R.id.edit_id);
95 | mEditId.setInputType(InputType.TYPE_NULL);
96 | mEditId.setOnTouchListener(new View.OnTouchListener() {
97 | public boolean onTouch(View v, MotionEvent event) {
98 | mEditId.setInputType(InputType.TYPE_CLASS_NUMBER);
99 | mEditId.onTouchEvent(event); // call native handler
100 | return true; // consume touch even
101 | }
102 | });
103 |
104 | Button buttonStart = (Button) findViewById(R.id.button_start);
105 | buttonStart.setOnClickListener(mButtonStartListener);
106 | }
107 |
108 |
109 | /**
110 | * Populates the options menu
111 | */
112 | public boolean onCreateOptionsMenu(Menu menu) {
113 | MenuInflater inflater = getMenuInflater();
114 | inflater.inflate(R.menu.subjective_menu, menu);
115 | return true;
116 | }
117 |
118 | /**
119 | * Handles the option menu selections
120 | */
121 | public boolean onOptionsItemSelected(MenuItem item) {
122 | switch (item.getItemId()) {
123 |
124 | // change to the preferences activity
125 | case R.id.menu_preferences:
126 | Intent prefIntent = new Intent();
127 | prefIntent.setClass(getApplicationContext(),
128 | SubjectivePlayerPreferences.class);
129 | startActivity(prefIntent);
130 | return true;
131 |
132 | // Display the about dialog
133 | case R.id.menu_about:
134 | showDialog(DIALOG_ABOUT_ID);
135 | return true;
136 |
137 | // Exit the application
138 | case R.id.menu_exit:
139 | SubjectivePlayer.this.finish();
140 | return true;
141 |
142 | default:
143 | return super.onOptionsItemSelected(item);
144 | }
145 | }
146 |
147 | /**
148 | * Handles the dialogs shown in the application
149 | */
150 | protected Dialog onCreateDialog(int id) {
151 | Dialog dialog = null;
152 | switch (id) {
153 |
154 | // display the about dialog
155 | case DIALOG_ABOUT_ID:
156 | AlertDialog.Builder builderAbout = new AlertDialog.Builder(this);
157 | builderAbout.setTitle(R.string.about_caption).setMessage(
158 | R.string.about_body).setCancelable(false)
159 | .setPositiveButton("Close",
160 | new DialogInterface.OnClickListener() {
161 | public void onClick(DialogInterface dialog,
162 | int id) {
163 | dialog.dismiss();
164 | }
165 | });
166 | dialog = (AlertDialog) builderAbout.create();
167 | break;
168 |
169 | // display an error dialog if the fields for participant ID, method or
170 | // config file are empty
171 | case DIALOG_EMPTY:
172 | AlertDialog.Builder builderEmpty = new AlertDialog.Builder(this);
173 | builderEmpty.setTitle(R.string.error_nodata_caption).setMessage(
174 | R.string.error_nodata_body).setCancelable(false)
175 | .setPositiveButton("Close",
176 | new DialogInterface.OnClickListener() {
177 | public void onClick(DialogInterface dialog,
178 | int id) {
179 | dialog.dismiss();
180 | }
181 | });
182 | dialog = (AlertDialog) builderEmpty.create();
183 | break;
184 |
185 | case DIALOG_ID_ALREADY_USED:
186 | AlertDialog.Builder builderUsed = new AlertDialog.Builder(this);
187 | builderUsed.setTitle(R.string.error_id_used_caption).setMessage(
188 | R.string.error_id_used_body).setCancelable(false)
189 | .setPositiveButton("Close",
190 | new DialogInterface.OnClickListener() {
191 | public void onClick(DialogInterface dialog,
192 | int id) {
193 | dialog.dismiss();
194 | }
195 | });
196 | dialog = (AlertDialog) builderUsed.create();
197 | break;
198 |
199 | default:
200 | dialog = null;
201 | }
202 | return dialog;
203 | }
204 |
205 | /*
206 | * Button Listeners
207 | */
208 |
209 | /**
210 | * Click listener for the file browser button
211 | */
212 | private OnClickListener mButtonFileBrowserListener = new OnClickListener() {
213 | public void onClick(View v) {
214 | showDialog(DIALOG_FILEBROWSER);
215 | }
216 | };
217 |
218 | /**
219 | * Click listener for the method selection button
220 | */
221 | private OnClickListener mButtonMethodBrowserListener = new OnClickListener() {
222 | public void onClick(View v) {
223 | showDialog(DIALOG_METHODBROWSER);
224 | }
225 | };
226 |
227 | /**
228 | * Click listener for the start button
229 | */
230 | private OnClickListener mButtonStartListener = new OnClickListener() {
231 | public void onClick(View v) {
232 | // get the contents of the ID
233 | EditText editId = (EditText) findViewById(R.id.edit_id);
234 | Editable editIdString = (Editable) editId.getText();
235 |
236 | // P.NATS: Only allow ACR
237 | Session.sCurrentMethod = Methods.TYPE_ACR_CATEGORICAL;
238 |
239 | if (editIdString.toString().equalsIgnoreCase("")) {
240 | showDialog(DIALOG_EMPTY);
241 | } else {
242 | Session.sParticipantId = Integer.parseInt(editIdString.toString());
243 |
244 | // set config file from ID
245 | Configuration.sFileConfig = new File(
246 | Configuration.sFolderApproot, "playlist" + Session.sParticipantId + "." + Configuration.sConfigSuffix);
247 |
248 | if (!(Configuration.sFileConfig.exists() && Configuration.sFileConfig.canRead())) {
249 | showDialog(DIALOG_EMPTY);
250 | return;
251 | }
252 |
253 | // check if ID hasn't been used already
254 | if (Configuration.sAllowDuplicateIds == false) {
255 | if (Logger.idExists(Session.sParticipantId)) {
256 | showDialog(DIALOG_ID_ALREADY_USED);
257 | return;
258 | }
259 | }
260 |
261 | Session.readVideosFromFile(Configuration.sFileConfig);
262 |
263 | // if we use continuous rating
264 | // TODO: refactor, this is unnecessarily duplicated
265 | if (Session.sCurrentMethod == Methods.TYPE_CONTINUOUS_RATING) {
266 | Intent sessionIntent = new Intent();
267 | sessionIntent.setClass(getApplicationContext(), SubjectivePlayerSession.class);
268 | startActivity(sessionIntent);
269 | }
270 | // else, use the classical method of rating (one video after another)
271 | else {
272 | Intent sessionIntent = new Intent();
273 | sessionIntent.setClass(getApplicationContext(),
274 | SubjectivePlayerSession.class);
275 | startActivity(sessionIntent);
276 | }
277 | }
278 | }
279 | };
280 |
281 | }
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/SubjectivePlayerPreferences.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | import android.os.Bundle;
20 | import android.preference.PreferenceActivity;
21 |
22 | public class SubjectivePlayerPreferences extends PreferenceActivity {
23 | /**
24 | * Called when the activity is first created.
25 | */
26 | @Override
27 | public void onCreate(Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 | addPreferencesFromResource(R.xml.preferences);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/org/univie/subjectiveplayer/SubjectivePlayerSession.java:
--------------------------------------------------------------------------------
1 | /* This file is part of SubjectivePlayer for Android.
2 | *
3 | * SubjectivePlayer for Android is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | *
8 | * SubjectivePlayer for Android is distributed in the hope that it will be useful,
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | * GNU General Public License for more details.
12 | *
13 | * You should have received a copy of the GNU General Public License
14 | * along with SubjectivePlayer for Android. If not, see .
15 | */
16 |
17 | package org.univie.subjectiveplayer;
18 |
19 | import java.io.File;
20 | import java.io.IOException;
21 | import java.util.ArrayList;
22 | import java.util.HashMap;
23 | import java.util.List;
24 |
25 | import android.app.Activity;
26 | import android.app.AlertDialog;
27 | import android.app.Dialog;
28 | import android.content.DialogInterface;
29 | import android.database.DataSetObserver;
30 | import android.media.AudioManager;
31 | import android.media.MediaPlayer;
32 | import android.media.MediaPlayer.OnCompletionListener;
33 | import android.media.MediaPlayer.OnErrorListener;
34 | import android.media.MediaPlayer.OnPreparedListener;
35 | import android.media.MediaPlayer.OnVideoSizeChangedListener;
36 | import android.os.Bundle;
37 | import android.preference.PreferenceManager;
38 | import android.util.Log;
39 | import android.view.KeyEvent;
40 | import android.view.LayoutInflater;
41 | import android.view.SurfaceHolder;
42 | import android.view.SurfaceView;
43 | import android.view.View;
44 | import android.view.SurfaceHolder.Callback;
45 | import android.view.ViewGroup;
46 | import android.view.ViewGroup.LayoutParams;
47 | import android.widget.AbsListView;
48 | import android.widget.Button;
49 | import android.widget.RadioButton;
50 | import android.widget.RadioGroup;
51 | //import android.widget.LinearLayout.LayoutParams;
52 | import android.widget.ListAdapter;
53 | import android.widget.ListView;
54 | import android.widget.SeekBar;
55 | import android.view.ContextThemeWrapper;
56 | import android.widget.TextView;
57 | import android.widget.Toast;
58 |
59 | /**
60 | * Plays the videos and shows dialogs the order set by the session data. This
61 | * class heavliy incorporates methods from the Android Developer Guide:
62 | * http://developer
63 | * .android.com/resources/samples/ApiDemos/src/com/example/android
64 | * /apis/media/MediaPlayerDemo_Video.html - Licensed under the Apache License,
65 | * Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0
66 | */
67 | public class SubjectivePlayerSession extends Activity implements Callback,
68 | OnCompletionListener, OnPreparedListener, OnVideoSizeChangedListener,
69 | OnErrorListener {
70 |
71 | private static final String TAG = SubjectivePlayerSession.class
72 | .getSimpleName();
73 |
74 | private View mSeekBarDialogView = null;
75 |
76 | private Button mOkButton = null;
77 |
78 | private static int sCurrentRating;
79 | private static final int RATING_MIN = 1;
80 | private static final int RATING_DEFAULT = 3;
81 | private static final int RATING_MAX = 5;
82 | /** The interval in milliseconds that the rating should be captured */
83 | private static final int RATING_INTERVAL = 1000;
84 |
85 | private LoggingThread mLoggingThread;
86 | private Thread mThread;
87 |
88 | private MediaPlayer mPlayer;
89 | private SurfaceView mPlayView;
90 | private SurfaceHolder mHolder;
91 | private int mVideoWidth;
92 | private int mVideoHeight;
93 | /** Determines whether a video is currently playing or not */
94 | private boolean mIsVideoPlaying = false;
95 | /**
96 | * Determines whether the video size is known or not. If not, the player
97 | * will not start
98 | */
99 | private boolean mIsVideoSizeKnown = false;
100 | /**
101 | * Determines whether a video is ready to be played back. If not, the player
102 | * will not start
103 | */
104 | private boolean mIsVideoReadyToBePlayed = false;
105 |
106 | private Dialog mDialog;
107 | private static final int DIALOG_ACR_CATEGORICAL = 0;
108 | private static final int DIALOG_DSIS_CATEGORICAL = 1;
109 | private static final int DIALOG_CONTINUOUS = 2;
110 | // P.NATS: new dialog
111 | private static final int DIALOG_ACR_CUSTOM = 3;
112 |
113 | private long mBackPressedTime = 0;
114 |
115 | private int mCurrentRating = 0;
116 |
117 | /**
118 | * Called when the activity is being started or restarted
119 | */
120 | @Override
121 | public void onResume() {
122 | super.onResume();
123 |
124 | // reload any updated preferences
125 | Configuration.setPreferences(PreferenceManager
126 | .getDefaultSharedPreferences(getBaseContext()));
127 |
128 | }
129 |
130 | /**
131 | * Called when the activity is first created.
132 | */
133 | @Override
134 | public void onCreate(Bundle savedInstanceState) {
135 | super.onCreate(savedInstanceState);
136 |
137 | setContentView(R.layout.video_view);
138 |
139 | try {
140 | mPlayView = (SurfaceView) findViewById(R.id.video_surface);
141 | mHolder = mPlayView.getHolder();
142 | mHolder.addCallback(this);
143 | mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
144 | } catch (Exception e) {
145 | Log.e(TAG, "Error while creating Surface:" + e.toString());
146 | }
147 | }
148 |
149 | @Override
150 | /**
151 | * Called when the activity pauses
152 | */
153 | protected void onPause() {
154 | super.onPause();
155 | releasePlayer();
156 | if (mLoggingThread != null) {
157 | mLoggingThread.stop();
158 | }
159 | cleanUp();
160 | }
161 |
162 | @Override
163 | /**
164 | * Called when the activity is destroyed.
165 | */
166 | protected void onDestroy() {
167 | super.onDestroy();
168 | releasePlayer();
169 | cleanUp();
170 | Session.reset();
171 | }
172 |
173 | /**
174 | * Called when the surface to display the video is created. This is only
175 | * called once, so the player is prepared for playing the first file of the
176 | * session.
177 | */
178 | public void surfaceCreated(SurfaceHolder holder) {
179 | preparePlayerForVideo(Session.sCurrentTrack);
180 | }
181 |
182 | public void surfaceChanged(SurfaceHolder holder, int format, int width,
183 | int height) {
184 |
185 | }
186 |
187 | public void surfaceDestroyed(SurfaceHolder holder) {
188 |
189 | }
190 |
191 | /**
192 | * Prepares a video player for the given video index of the session. The
193 | * player will start automatically when it has completed preparing, so it is
194 | * only necessary to call this method which will then trigger all other
195 | * calls and listeners.
196 | *
197 | * @param videoIndex
198 | * The index of the video in the session.
199 | * @throws IllegalArgumentException
200 | * @throws IllegalStateException
201 | * When a command was executed on the player that doesn't work
202 | * in the current state.
203 | * @throws IOException
204 | * When the input file was not found.
205 | * @throws ArrayIndexOutOfBoundsException
206 | * When the video index exceeded the array size.
207 | */
208 | public void preparePlayerForVideo(int videoIndex) {
209 | try {
210 |
211 | if (Session.sCurrentMethod == Methods.TYPE_CONTINUOUS_RATING) {
212 | String videoName = Session.sTracks.get(videoIndex);
213 | Log.d(TAG, "Creating new logging thread for video " + videoName);
214 | mLoggingThread = new LoggingThread(videoName);
215 | sCurrentRating = RATING_DEFAULT;
216 | }
217 |
218 | mPlayer = new MediaPlayer();
219 |
220 | String videoPath = getPathFromPlaylist(videoIndex);
221 | File videoFile = new File(videoPath);
222 | if ((!videoFile.exists()) || (!videoFile.canRead())) {
223 | throw new IOException("Video file " + videoPath + "not found!");
224 | }
225 | mPlayer.setDataSource(videoPath);
226 | mPlayer.setDisplay(mHolder);
227 | mPlayer.setScreenOnWhilePlaying(true);
228 | mPlayer.setOnPreparedListener(this);
229 | mPlayer.setOnCompletionListener(this);
230 | mPlayer.setOnVideoSizeChangedListener(this);
231 | mPlayer.setOnErrorListener(this);
232 | mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
233 | mPlayer.prepare();
234 | } catch (IllegalArgumentException e) {
235 | Log.e(TAG, e.toString());
236 | e.printStackTrace();
237 | } catch (IllegalStateException e) {
238 | Log.e(TAG, e.toString());
239 | e.printStackTrace();
240 | } catch (IOException e) {
241 | Log.e(TAG, e.toString());
242 | e.printStackTrace();
243 | } catch (ArrayIndexOutOfBoundsException e) {
244 | Log.e(TAG, e.toString());
245 | e.printStackTrace();
246 | }
247 | }
248 |
249 | /**
250 | * Called when the Media Player is finished preparing and ready to play.
251 | */
252 | public void onPrepared(MediaPlayer player) {
253 | mIsVideoReadyToBePlayed = true;
254 | if (mIsVideoReadyToBePlayed && mIsVideoSizeKnown) {
255 | startVideo();
256 | }
257 | }
258 |
259 | /**
260 | * Called when the video size changes.
261 | */
262 | public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
263 | Log.v(TAG, "onVideoSizeChanged called");
264 | if (width == 0 || height == 0) {
265 | Log.e(TAG, "invalid video width(" + width + ") or height(" + height
266 | + ")");
267 | return;
268 | }
269 | mIsVideoSizeKnown = true;
270 |
271 | mVideoWidth = width;
272 | mVideoHeight = height;
273 |
274 | if (mIsVideoReadyToBePlayed && mIsVideoSizeKnown) {
275 | startVideo();
276 | }
277 | }
278 |
279 | /**
280 | * Called when the Media Player finished playing its file.
281 | */
282 | public void onCompletion(MediaPlayer player) {
283 | // release the player and reset
284 | releasePlayer();
285 | cleanUp();
286 |
287 | switch (Session.sCurrentMethod) {
288 | case Methods.TYPE_ACR_CATEGORICAL:
289 | // P.NATS
290 | //showDialog(DIALOG_ACR_CATEGORICAL);
291 | showDialog(DIALOG_ACR_CUSTOM);
292 | break;
293 | case Methods.TYPE_DSIS_CATEGORICAL:
294 | break;
295 | case Methods.TYPE_CONTINUOUS:
296 | showDialog(DIALOG_CONTINUOUS);
297 | break;
298 | case Methods.TYPE_CONTINUOUS_RATING:
299 | nextVideo();
300 | break;
301 | default:
302 | finish();
303 | }
304 | }
305 |
306 | public boolean onError(MediaPlayer mp, int what, int extra) {
307 | switch (what) {
308 | case (MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING):
309 | Log.e(TAG, "The media player can't decode fast enough.");
310 | finishSession();
311 | break;
312 | }
313 | return false;
314 | }
315 |
316 | /**
317 | * Starts the player.
318 | */
319 | public void startVideo() {
320 | // LayoutParams mParams = new LayoutParams(mVideoWidth, mVideoHeight);
321 | LayoutParams mParams = mPlayView.getLayoutParams();
322 |
323 | // scale to screen width
324 | // http://stackoverflow.com/q/4835060
325 | // Get the width of the screen
326 | int screenWidth = getWindowManager().getDefaultDisplay().getWidth();
327 | // Set the width of the SurfaceView to the width of the screen
328 | mParams.width = screenWidth;
329 | // Set the height of the SurfaceView to match the aspect ratio of the video
330 | // be sure to cast these as floats otherwise the calculation will likely be 0
331 | mParams.height = (int) (((float)mVideoHeight / (float)mVideoWidth) * (float)screenWidth);
332 |
333 | mPlayView.setLayoutParams(mParams);
334 | mIsVideoPlaying = true;
335 |
336 | if (Session.sCurrentMethod == Methods.TYPE_CONTINUOUS_RATING) {
337 | Log.d(TAG, "Running thread");
338 | mThread = new Thread(mLoggingThread);
339 | mThread.start();
340 | }
341 |
342 | mPlayer.start();
343 | }
344 |
345 | /**
346 | * Resets all variables of the player.
347 | */
348 | private void cleanUp() {
349 | mVideoWidth = 0;
350 | mVideoHeight = 0;
351 | mIsVideoReadyToBePlayed = false;
352 | mIsVideoSizeKnown = false;
353 | mIsVideoPlaying = false;
354 | if (mLoggingThread != null) {
355 | mLoggingThread.stop();
356 | }
357 | }
358 |
359 | /**
360 | * Releases the player.
361 | */
362 | private void releasePlayer() {
363 | if (mPlayer != null) {
364 | mPlayer.release();
365 | mPlayer = null;
366 | }
367 | }
368 |
369 | /**
370 | * Returns the correct path for a video file at the given index. The path is
371 | * composed from the Configuration, i.e. the directory where the videos
372 | * reside and the file name of the current video. This method does not check
373 | * whether a file exists or not.
374 | *
375 | * @param index
376 | * The index of the video in the session
377 | * @return The composed path of the video file
378 | * @throws ArrayIndexOutOfBoundsException
379 | * When an index is requested that is not available
380 | * @throws IOException
381 | * When a file is not found
382 | */
383 | private String getPathFromPlaylist(int index)
384 | throws ArrayIndexOutOfBoundsException {
385 | File file = new File(Configuration.sFolderVideos,
386 | Session.sTracks.get(index));
387 | String path = file.getPath();
388 | Log.d(TAG, "Set data source to: " + path);
389 | return path;
390 | }
391 |
392 | /**
393 | * Finishes the current rating session by cleaning up the player and writing
394 | * the rating logs.
395 | */
396 | private void finishSession() {
397 | cleanUp();
398 | releasePlayer();
399 | if (Session.sCurrentMethod != Methods.TYPE_CONTINUOUS_RATING) {
400 | Logger.writeSessionLogCSV();
401 | }
402 | Session.reset();
403 | finish();
404 | }
405 |
406 | /**
407 | * Starts the next video after a dialog has been closed by the user
408 | */
409 | private void nextVideo() {
410 | // show the next video if possible
411 | Session.sCurrentTrack++;
412 | if (Session.sCurrentTrack < Session.sTracks.size()) {
413 | if (Session.sCurrentMethod == Methods.TYPE_CONTINUOUS) {
414 | SubjectivePlayerSession.this.removeDialog(DIALOG_CONTINUOUS);
415 | }
416 | if (Session.sCurrentMethod == Methods.TYPE_ACR_CATEGORICAL) {
417 | SubjectivePlayerSession.this.removeDialog(DIALOG_ACR_CATEGORICAL);
418 | }
419 | preparePlayerForVideo(Session.sCurrentTrack);
420 | } else {
421 | finishSession();
422 | }
423 | }
424 |
425 | /**
426 | * KeyDown handler for the Volume Up / Volume Down buttons
427 | */
428 | public boolean onKeyDown(int keyCode, KeyEvent event) {
429 |
430 | if (Session.sCurrentMethod == Methods.TYPE_ACR_CATEGORICAL) {
431 | return super.onKeyDown(keyCode, event);
432 | }
433 |
434 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
435 | || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
436 |
437 | if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
438 | && (sCurrentRating > RATING_MIN)) {
439 | sCurrentRating--;
440 | }
441 |
442 | if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP)
443 | && (sCurrentRating < RATING_MAX)) {
444 | sCurrentRating++;
445 | }
446 |
447 | Log.d(TAG, "Current rating: " + sCurrentRating);
448 |
449 | return true;
450 | } else {
451 | return super.onKeyDown(keyCode, event);
452 | }
453 | }
454 |
455 | public class LoggingThread implements Runnable {
456 | String videoName;
457 |
458 | public LoggingThread(String videoName) {
459 | Log.d(TAG, "Created logging thread for " + videoName);
460 | this.videoName = videoName;
461 | }
462 |
463 | /**
464 | * Runs the logging thread in the background
465 | */
466 | public void run() {
467 | Log.d(TAG, "Running the thread for " + videoName);
468 | try {
469 | Logger.startContinuousLogCSV(videoName);
470 | while (mIsVideoPlaying) {
471 | Logger.writeContinuousData(videoName, "" + mPlayer.getCurrentPosition(), "" + sCurrentRating);
472 | Thread.sleep(RATING_INTERVAL);
473 | }
474 | Log.d(TAG, "Video " + videoName + " not playing anymore, stopping.");
475 | Logger.closeContinuousLogCSV();
476 | } catch (Exception e) {
477 | Logger.closeContinuousLogCSV();
478 | }
479 | }
480 |
481 | public void stop() {
482 | Logger.closeContinuousLogCSV();
483 | Log.d(TAG, "Stopped logging thread for " + videoName);
484 | }
485 | }
486 |
487 | /**
488 | * Closes the currently active dialog
489 | */
490 | private void dismissCurrentDialog() {
491 | if (mDialog != null) {
492 | mDialog.dismiss();
493 | }
494 | }
495 |
496 | /**
497 | * Handles the dialogs shown in the application
498 | */
499 | protected Dialog onCreateDialog(int id) {
500 | //Dialog dialog = null;
501 |
502 | switch (id) {
503 | // display a dialog asking the user to rate ACR quality
504 | case DIALOG_ACR_CATEGORICAL:
505 | // P.NATS: reduce font size
506 | ContextThemeWrapper cw = new ContextThemeWrapper(this, R.style.AlertDialogTheme);
507 | AlertDialog.Builder builderACR = new AlertDialog.Builder(cw);
508 | builderACR.setTitle(R.string.rate_ACR_caption);
509 | builderACR.setItems(R.array.acr_labels,
510 | new DialogInterface.OnClickListener() {
511 | public void onClick(DialogInterface dialog, int item) {
512 | int rating = item;
513 | Session.sRatings.add(rating);
514 | Session.sRatingTime.add(System.currentTimeMillis());
515 | dialog.dismiss();
516 | nextVideo();
517 | }
518 | });
519 | mDialog = (AlertDialog) builderACR.create();
520 |
521 | // Hack to make the list items smaller
522 | // http://stackoverflow.com/questions/24367612/how-can-i-set-an-alertdialog-item-height
523 | mDialog.setOnShowListener(new DialogInterface.OnShowListener() {
524 | @Override
525 | public void onShow(DialogInterface dialogInterface) {
526 | ListView listView = ((AlertDialog) dialogInterface).getListView();
527 | final ListAdapter originalAdapter = listView.getAdapter();
528 |
529 | listView.setAdapter(new ListAdapter() {
530 | @Override
531 | public int getCount() {
532 | return originalAdapter.getCount();
533 | }
534 |
535 | @Override
536 | public Object getItem(int id) {
537 | return originalAdapter.getItem(id);
538 | }
539 |
540 | @Override
541 | public long getItemId(int id) {
542 | return originalAdapter.getItemId(id);
543 | }
544 |
545 | @Override
546 | public int getItemViewType(int id) {
547 | return originalAdapter.getItemViewType(id);
548 | }
549 |
550 | @Override
551 | public View getView(int position, View convertView, ViewGroup parent) {
552 | View view = originalAdapter.getView(position, convertView, parent);
553 | TextView textView = (TextView) view;
554 | //textView.setTextSize(16); set text size programmatically if needed
555 | textView.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, 100 /* this is item height */));
556 | return view;
557 | }
558 |
559 | @Override
560 | public int getViewTypeCount() {
561 | return originalAdapter.getViewTypeCount();
562 | }
563 |
564 | @Override
565 | public boolean hasStableIds() {
566 | return originalAdapter.hasStableIds();
567 | }
568 |
569 | @Override
570 | public boolean isEmpty() {
571 | return originalAdapter.isEmpty();
572 | }
573 |
574 | @Override
575 | public void registerDataSetObserver(DataSetObserver observer) {
576 | originalAdapter.registerDataSetObserver(observer);
577 |
578 | }
579 |
580 | @Override
581 | public void unregisterDataSetObserver(DataSetObserver observer) {
582 | originalAdapter.unregisterDataSetObserver(observer);
583 |
584 | }
585 |
586 | @Override
587 | public boolean areAllItemsEnabled() {
588 | return originalAdapter.areAllItemsEnabled();
589 | }
590 |
591 | @Override
592 | public boolean isEnabled(int position) {
593 | return originalAdapter.isEnabled(position);
594 | }
595 |
596 | });
597 | }
598 | });
599 |
600 | break;
601 |
602 | // P.NATS
603 | case DIALOG_ACR_CUSTOM:
604 | mDialog = new CustomDialog(this);
605 | mDialog.setContentView(R.layout.dialog_acr_custom);
606 | mDialog.setCancelable(false);
607 |
608 | // button listeners
609 | final List radioButtonList = new ArrayList();
610 | RadioButton acrButtonExcellent = (RadioButton) mDialog.findViewById(R.id.radioButtonExcellent);
611 | RadioButton acrButtonGood = (RadioButton) mDialog.findViewById(R.id.radioButtonGood);
612 | RadioButton acrButtonFair = (RadioButton) mDialog.findViewById(R.id.radioButtonFair);
613 | RadioButton acrButtonPoor = (RadioButton) mDialog.findViewById(R.id.radioButtonPoor);
614 | RadioButton acrButtonBad = (RadioButton) mDialog.findViewById(R.id.radioButtonBad);
615 |
616 | // Just some IDs
617 | acrButtonExcellent.setId(0);
618 | acrButtonGood.setId(1);
619 | acrButtonFair.setId(2);
620 | acrButtonPoor.setId(3);
621 | acrButtonBad.setId(4);
622 |
623 | // Add all buttons to "group"
624 | radioButtonList.add(acrButtonExcellent);
625 | radioButtonList.add(acrButtonGood);
626 | radioButtonList.add(acrButtonFair);
627 | radioButtonList.add(acrButtonPoor);
628 | radioButtonList.add(acrButtonBad);
629 |
630 | // ACR values
631 | final HashMap