Handle rotated videos (by metadata).
Use "exiftool" to determine video metadata. Allow for user specified ffmpeg binary. Embedded Jetty. Sample videos.
This commit is contained in:
parent
bdbcf6b030
commit
7b2bfc368c
9 changed files with 308 additions and 56 deletions
BIN
photos/video/IMG_0841.MOV
Normal file
BIN
photos/video/IMG_0841.MOV
Normal file
Binary file not shown.
BIN
photos/video/IMG_0842.MOV
Normal file
BIN
photos/video/IMG_0842.MOV
Normal file
Binary file not shown.
BIN
photos/video/IMG_0843.MOV
Normal file
BIN
photos/video/IMG_0843.MOV
Normal file
Binary file not shown.
BIN
photos/video/IMG_0844.MOV
Normal file
BIN
photos/video/IMG_0844.MOV
Normal file
Binary file not shown.
25
pom.xml
25
pom.xml
|
|
@ -174,22 +174,22 @@
|
|||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.5.10</version>
|
||||
<version>1.6.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
<version>1.5.10</version>
|
||||
<version>1.6.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jul-to-slf4j</artifactId>
|
||||
<version>1.5.10</version>
|
||||
<version>1.6.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<version>1.5.10</version>
|
||||
<version>1.6.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
|
|
@ -198,6 +198,23 @@
|
|||
<type>jar</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-mapper-asl</artifactId>
|
||||
<version>1.9.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>7.6.8.v20121106</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-webapp</artifactId>
|
||||
<version>7.6.8.v20121106</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<repositories>
|
||||
<repository>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
|
||||
import org.forkalsrud.album.exif.Dimension;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FlvMetadata {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(FlvMetadata.class);
|
||||
|
||||
private abstract class Attr<T> {
|
||||
|
||||
|
|
@ -98,7 +101,7 @@ public class FlvMetadata {
|
|||
public void read(InputStream in) throws FlvFormatException, IOException {
|
||||
int type = in.read();
|
||||
if (type != 0) {
|
||||
throw new FlvFormatException("Not a boolean: " + type);
|
||||
throw new FlvFormatException("Not a number: " + type);
|
||||
}
|
||||
set(readDouble(in));
|
||||
}
|
||||
|
|
@ -167,7 +170,7 @@ public class FlvMetadata {
|
|||
throw new FlvFormatException("Not an object: " + type);
|
||||
}
|
||||
if (!in.markSupported()) {
|
||||
throw new FlvFormatException("Need redahead");
|
||||
throw new FlvFormatException("Need readahead");
|
||||
}
|
||||
List<Integer> filePositions = null;
|
||||
List<Double> times = null;
|
||||
|
|
@ -203,6 +206,14 @@ public class FlvMetadata {
|
|||
|
||||
private StringAttr creator = new StringAttr("creator");
|
||||
private StringAttr metadataCreator = new StringAttr("metadatacreator");
|
||||
private StringAttr majorBrand = new StringAttr("major_brand");
|
||||
private StringAttr minorVersion = new StringAttr("minor_version");
|
||||
private StringAttr compatibleBrands = new StringAttr("compatible_brands");
|
||||
private StringAttr creationTime = new StringAttr("creation_time");
|
||||
private StringAttr encoder = new StringAttr("encoder");
|
||||
private StringAttr encoderEng = new StringAttr( "encoder-eng");
|
||||
private StringAttr date = new StringAttr("date");
|
||||
private StringAttr dateEng = new StringAttr("date-eng");
|
||||
|
||||
private BooleanAttr hasKeyframes = new BooleanAttr("hasKeyframes");
|
||||
private BooleanAttr hasVideo = new BooleanAttr("hasVideo");
|
||||
|
|
@ -247,6 +258,15 @@ public class FlvMetadata {
|
|||
|
||||
attrs.add(creator);
|
||||
attrs.add(metadataCreator);
|
||||
attrs.add(majorBrand);
|
||||
attrs.add(minorVersion);
|
||||
attrs.add(compatibleBrands);
|
||||
attrs.add(creationTime);
|
||||
attrs.add(date);
|
||||
attrs.add(dateEng);
|
||||
attrs.add(encoder);
|
||||
attrs.add(encoderEng);
|
||||
|
||||
attrs.add(hasKeyframes);
|
||||
attrs.add(hasVideo);
|
||||
attrs.add(hasAudio);
|
||||
|
|
@ -511,10 +531,10 @@ public class FlvMetadata {
|
|||
throw new FlvFormatException("Not an ECMA array: " + type);
|
||||
}
|
||||
int arrayLen = readEcmaArrayHeader(in);
|
||||
for (int i = 0; i < arrayLen; i++) {
|
||||
while (!isLookingAtEnd(in)) {
|
||||
readProperty(in);
|
||||
}
|
||||
readEcmaArrayFooter(in);
|
||||
readEcmaArrayFoter(in);
|
||||
} catch (FlvFormatException e) {
|
||||
LoggerFactory.getLogger(getClass()).error("invalid", e);
|
||||
}
|
||||
|
|
@ -583,7 +603,7 @@ public class FlvMetadata {
|
|||
return len;
|
||||
}
|
||||
|
||||
protected void readEcmaArrayFooter(InputStream in) throws FlvFormatException, IOException {
|
||||
protected void readEcmaArrayFoter(InputStream in) throws FlvFormatException, IOException {
|
||||
readObjectEnd(in);
|
||||
}
|
||||
|
||||
|
|
@ -607,10 +627,26 @@ public class FlvMetadata {
|
|||
|
||||
String name = readString(in);
|
||||
Attr attr = findAttrByName(name);
|
||||
if (attr == null) {
|
||||
throw new FlvFormatException("Unknown attribute: " + name);
|
||||
}
|
||||
attr.read(in);
|
||||
if (attr != null) {
|
||||
attr.read(in);
|
||||
} else {
|
||||
log.warn("Unknown metadata property: " + name);
|
||||
readUnknownProperty(in, name);
|
||||
}
|
||||
}
|
||||
|
||||
protected void readUnknownProperty(InputStream in, String name) throws IOException, FlvFormatException {
|
||||
|
||||
in.mark(1);
|
||||
int type = in.read();
|
||||
in.reset();
|
||||
switch (type) {
|
||||
case 2:
|
||||
new StringAttr(name).read(in);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Attr findAttrByName(String name) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.forkalsrud.album.db.Chunk;
|
||||
import org.forkalsrud.album.db.MovieDatabase;
|
||||
import org.forkalsrud.album.exif.Dimension;
|
||||
|
|
@ -27,6 +28,7 @@ public class MovieCoder {
|
|||
|
||||
private String ffmpegExecutable;
|
||||
private String mplayerExecutable;
|
||||
private String exiftoolExecutable;
|
||||
private PictureScaler pictureScaler;
|
||||
private MovieDatabase movieDb;
|
||||
private HashMap<String, EncodingProcess> currentEncodings = new HashMap<String, EncodingProcess>();
|
||||
|
|
@ -39,15 +41,16 @@ public class MovieCoder {
|
|||
public void init() throws Exception {
|
||||
|
||||
ExecUtil util = new ExecUtil();
|
||||
this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg");
|
||||
if (this.ffmpegExecutable == null) {
|
||||
this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg");
|
||||
}
|
||||
this.mplayerExecutable = util.findExecutableInShellPath("mplayer");
|
||||
this.exiftoolExecutable = util.findExecutableInShellPath("exiftool");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param f the movie file
|
||||
|
|
@ -57,35 +60,159 @@ public class MovieCoder {
|
|||
*/
|
||||
public Map<String, String> generateVideoProperties(File f) throws IOException, InterruptedException {
|
||||
|
||||
/*
|
||||
* [{
|
||||
"SourceFile": "/home/erik/local/IMG_0837.mov",
|
||||
"ExifToolVersion": 9.12,
|
||||
"FileName": "IMG_0837.mov",
|
||||
"Directory": "/home/erik/local",
|
||||
"FileSize": "1438 kB",
|
||||
"FileModifyDate": "2013:01:19 18:36:53-08:00",
|
||||
"FileAccessDate": "2013:01:19 18:36:57-08:00",
|
||||
"FileInodeChangeDate": "2013:01:19 18:36:53-08:00",
|
||||
"FilePermissions": "rw-rw-r--",
|
||||
"FileType": "MOV",
|
||||
"MIMEType": "video/quicktime",
|
||||
"MajorBrand": "Apple QuickTime (.MOV/QT)",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["qt "],
|
||||
"MovieDataSize": 1467141,
|
||||
"MovieHeaderVersion": 0,
|
||||
"ModifyDate": "2012:12:30 06:44:31",
|
||||
"TimeScale": 600,
|
||||
"Duration": "9.07 s",
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": "100.00%",
|
||||
"PreviewTime": "0 s",
|
||||
"PreviewDuration": "0 s",
|
||||
"PosterTime": "0 s",
|
||||
"SelectionTime": "0 s",
|
||||
"SelectionDuration": "0 s",
|
||||
"CurrentTime": "0 s",
|
||||
"NextTrackID": 3,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "2012:12:30 06:44:26",
|
||||
"TrackModifyDate": "2012:12:30 06:44:31",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": "9.05 s",
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": "100.00%",
|
||||
"Balance": 0,
|
||||
"AudioChannels": 1,
|
||||
"AudioBitsPerSample": 16,
|
||||
"AudioSampleRate": 44100,
|
||||
"AudioFormat": "chan",
|
||||
"MatrixStructure": "0 1 0 -1 0 0 540 0 1",
|
||||
"ImageWidth": 960,
|
||||
"ImageHeight": 540,
|
||||
"CleanApertureDimensions": "960x540",
|
||||
"ProductionApertureDimensions": "960x540",
|
||||
"EncodedPixelsDimensions": "960x540",
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "2012:12:30 06:44:26",
|
||||
"MediaModifyDate": "2012:12:30 06:44:31",
|
||||
"MediaTimeScale": 600,
|
||||
"MediaDuration": "9.10 s",
|
||||
"MediaLanguageCode": "und",
|
||||
"GraphicsMode": "ditherCopy",
|
||||
"OpColor": "32768 32768 32768",
|
||||
"HandlerClass": "Data Handler",
|
||||
"HandlerVendorID": "Apple",
|
||||
"HandlerDescription": "Core Media Data Handler",
|
||||
"CompressorID": "avc1",
|
||||
"SourceImageWidth": 960,
|
||||
"SourceImageHeight": 540,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"CompressorName": "H.264",
|
||||
"BitDepth": 24,
|
||||
"VideoFrameRate": 30,
|
||||
"CameraIdentifier": "Back",
|
||||
"FrameReadoutTime": "28512 microseconds",
|
||||
"Make": "Apple",
|
||||
"SoftwareVersion": "6.0.1",
|
||||
"CreateDate": "2012:12:29 16:30:21-08:00",
|
||||
"Model": "iPhone 4S",
|
||||
"HandlerType": "Metadata Tags",
|
||||
"Make-und-US": "Apple",
|
||||
"CreationDate-und-US": "2012:12:29 16:30:21-08:00",
|
||||
"Software-und-US": "6.0.1",
|
||||
"Model-und-US": "iPhone 4S",
|
||||
"AvgBitrate": "1.29 Mbps",
|
||||
"ImageSize": "960x540",
|
||||
"Rotation": 90
|
||||
}]
|
||||
*/
|
||||
Map<String, String> props = new HashMap<String, String>();
|
||||
ProcessBuilder pb = new ProcessBuilder().command(
|
||||
mplayerExecutable, "-vo", "null", "-ao", "null", "-frames", "0", "-identify", f.getAbsolutePath());
|
||||
this.exiftoolExecutable, "-j", f.getAbsolutePath());
|
||||
pb.redirectErrorStream(false);
|
||||
Process p = pb.start();
|
||||
p.getOutputStream().close();
|
||||
List<String> lines = IOUtils.readLines(p.getInputStream());
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String,Object>> userDataList = mapper.readValue(p.getInputStream(), List.class);
|
||||
p.waitFor();
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||
String width = "", height = "", length = "";
|
||||
for (String line : lines) {
|
||||
int pos = line.indexOf('=');
|
||||
if (pos >= 0) {
|
||||
String name = line.substring(0, pos);
|
||||
String value = line.substring(pos + 1);
|
||||
if (name.equals("ID_VIDEO_WIDTH")) width = value;
|
||||
if (name.equals("ID_VIDEO_HEIGHT")) height = value;
|
||||
if (name.equals("ID_LENGTH")) length = value;
|
||||
}
|
||||
}
|
||||
|
||||
props.put("type", "movie");
|
||||
props.put("orientation", "1");
|
||||
props.put("dimensions", new Dimension(width, height).toString());
|
||||
|
||||
Map<String, Object> userData = userDataList.get(0);
|
||||
|
||||
System.out.println(userData);
|
||||
{
|
||||
// The orientation is about flipping and rotating. Here is what an 'F' looks like
|
||||
// on pictures with each orientation.
|
||||
//
|
||||
// 1 2 3 4 5 6 7 8
|
||||
//
|
||||
// 888888 888888 88 88 8888888888 88 88 8888888888
|
||||
// 88 88 88 88 88 88 88 88 88 88 88 88
|
||||
// 8888 8888 8888 8888 88 8888888888 8888888888 88
|
||||
// 88 88 88 88
|
||||
// 88 88 888888 888888
|
||||
//
|
||||
// The first four are obtained with only flipping X and/or Y
|
||||
// The last four are obtained by rotating 90 degrees and then flipping X and/or Y.
|
||||
//
|
||||
String orientation;
|
||||
Object rotationObj = userData.get("Rotation");
|
||||
if (rotationObj == null) {
|
||||
orientation = "1";
|
||||
} else {
|
||||
String rotation = rotationObj.toString();
|
||||
if ("0".equals(rotation)) {
|
||||
orientation = "1";
|
||||
} else if ("90".equals(rotation)) {
|
||||
orientation = "6";
|
||||
} else if ("180".equals(rotation)) {
|
||||
orientation = "3";
|
||||
} else if ("270".equals(rotation)) {
|
||||
orientation = "8";
|
||||
} else {
|
||||
log.warn("unknown rotation: " + rotation + " for file " + f);
|
||||
orientation = "1";
|
||||
}
|
||||
}
|
||||
props.put("orientation", orientation);
|
||||
}
|
||||
|
||||
props.put("dimensions", userData.get("ImageSize").toString());
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||
props.put("captureDate", sdf.format(new Date(f.lastModified())));
|
||||
|
||||
props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode()));
|
||||
props.put("length", length);
|
||||
for (String prop : new String[] { "Duration", "MediaDuration", "PlayDuration" }) {
|
||||
Object o = userData.get(prop);
|
||||
if (o != null) {
|
||||
props.put("length", o.toString().split(" ")[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
|
||||
public File createTempDirectory() throws IOException {
|
||||
|
||||
final File temp = File.createTempFile("temp", Long.toString(System.nanoTime()));
|
||||
|
|
@ -160,6 +287,7 @@ public class MovieCoder {
|
|||
private FlvFilter filter;
|
||||
private String dbKey;
|
||||
private long fileTimestamp;
|
||||
private int orientation;
|
||||
|
||||
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) {
|
||||
this.file = file;
|
||||
|
|
@ -171,6 +299,7 @@ public class MovieCoder {
|
|||
FlvMetadata extraMeta = new FlvMetadata();
|
||||
extraMeta.setDuration(thumbnail.getDuration());
|
||||
this.filter = new FlvFilter(this, extraMeta);
|
||||
this.orientation = thumbnail.getOrientation();
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -190,28 +319,49 @@ public class MovieCoder {
|
|||
@Override
|
||||
public void run() {
|
||||
|
||||
// -vf transpose=1 (rotate 90)
|
||||
// -vf transpose=2 (rotate 270)
|
||||
// -vf vflip,hflip (rotate 180)
|
||||
|
||||
// 1 2 3 4 5 6 7 8
|
||||
//
|
||||
// 888888 888888 88 88 8888888888 88 88 8888888888
|
||||
// 88 88 88 88 88 88 88 88 88 88 88 88
|
||||
// 8888 8888 8888 8888 88 8888888888 8888888888 88
|
||||
// 88 88 88 88
|
||||
// 88 88 888888 888888
|
||||
String vf = new String[] {
|
||||
null, // 1
|
||||
"hflip", // 2
|
||||
"vflip,hflip", // 3
|
||||
"vflip", // 4
|
||||
"transpose=1,hflip", // 5
|
||||
"transpose=1", // 6
|
||||
"transpose=2,hflip", // 7
|
||||
"transpose=2", // 8
|
||||
}[orientation - 1];
|
||||
|
||||
try {
|
||||
/*
|
||||
ProcessBuilder pb = new ProcessBuilder().command(
|
||||
ffmpegExecutable, "-i", file.getAbsolutePath(),
|
||||
"-aspect", (thumbnail.getSize().getWidth() + ":" + thumbnail.getSize().getHeight()),
|
||||
"-s", (targetSize.getWidth() + "x" + targetSize.getHeight()),
|
||||
"-b", "1000000",
|
||||
"-crf", "25",
|
||||
// "-vcodec", "libx264", "-vpre", "knut_low",
|
||||
// "-acodec", "libfaac", "-aq", "100",
|
||||
"-f", "mpeg",
|
||||
"-");
|
||||
*/
|
||||
ProcessBuilder pb = new ProcessBuilder().command(
|
||||
ffmpegExecutable, "-i", file.getAbsolutePath(),
|
||||
// "-aspect", (thumbnail.getSize().getWidth() + ":" + thumbnail.getSize().getHeight()),
|
||||
"-s", (targetSize.getWidth() + "x" + targetSize.getHeight()),
|
||||
"-crf", "30",
|
||||
"-acodec", "libmp3lame", "-ar", "22050", "-vcodec", "libx264",
|
||||
"-g", "150", "-vpre", "medium",
|
||||
"-f", "flv",
|
||||
"-");
|
||||
|
||||
ArrayList<String> command = new ArrayList<String>();
|
||||
command.add(ffmpegExecutable);
|
||||
command.add("-i");
|
||||
command.add(file.getAbsolutePath());
|
||||
command.add("-s");
|
||||
command.add(targetSize.getWidth() + "x" + targetSize.getHeight());
|
||||
command.add("-crf");
|
||||
command.add("30");
|
||||
command.add("-acodec"); command.add("libmp3lame");
|
||||
command.add("-ar"); command.add("22050");
|
||||
command.add("-vcodec"); command.add("libx264");
|
||||
command.add("-g"); command.add("150");
|
||||
if (vf != null) {
|
||||
command.add("-vf");
|
||||
command.add(vf);
|
||||
}
|
||||
command.add("-f"); command.add("flv");
|
||||
command.add("-");
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
|
||||
log.info(pb.command().toString());
|
||||
pb.redirectErrorStream(false);
|
||||
|
|
@ -470,4 +620,15 @@ public class MovieCoder {
|
|||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setFfmpegPath(String property) {
|
||||
|
||||
if (property != null) {
|
||||
File program = new File(property);
|
||||
if (program.canExecute()) {
|
||||
this.ffmpegExecutable = property;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,17 @@ public class AlbumServlet
|
|||
basePrefix = "/" + base.getName();
|
||||
|
||||
String dbDirName = props.getProperty("dbdir");
|
||||
File dbDir = dbDirName != null ? new File(dbDirName) : new File(System.getProperty("java.io.tmpdir"), "album");
|
||||
File dbDir;
|
||||
if (dbDirName != null) {
|
||||
dbDir = new File(dbDirName);
|
||||
} else {
|
||||
dbDir = new File(System.getProperty("java.io.tmpdir"), "album");
|
||||
if (dbDir.isDirectory() && dbDir.canWrite()) {
|
||||
for (File f : dbDir.listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
dbDir.mkdirs();
|
||||
|
||||
EnvironmentConfig environmentConfig = new EnvironmentConfig();
|
||||
|
|
@ -158,6 +168,7 @@ public class AlbumServlet
|
|||
pictureScaler = new PictureScaler();
|
||||
|
||||
movieCoder = new MovieCoder(pictureScaler, movieDb);
|
||||
movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path"));
|
||||
try {
|
||||
movieCoder.init();
|
||||
} catch (Exception e) {
|
||||
|
|
@ -320,7 +331,7 @@ public class AlbumServlet
|
|||
thumbDb.store(key, cimg);
|
||||
log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
|
||||
} catch (Exception e) {
|
||||
e.fillInStackTrace();
|
||||
//e.fillInStackTrace();
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/test/java/org/forkalsrud/album/Runner.java
Normal file
27
src/test/java/org/forkalsrud/album/Runner.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package org.forkalsrud.album;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
|
||||
public class Runner {
|
||||
|
||||
/**
|
||||
* @param args
|
||||
* @throws Exception
|
||||
*/
|
||||
public static void main(String[] args) throws Exception {
|
||||
Server server = new Server(8080);
|
||||
|
||||
WebAppContext context = new WebAppContext();
|
||||
context.setDescriptor("src/main/webapp/WEB-INF/web.xml");
|
||||
context.setResourceBase("src/main/webapp");
|
||||
context.setContextPath("/");
|
||||
context.setParentLoaderPriority(true);
|
||||
|
||||
server.setHandler(context);
|
||||
|
||||
server.start();
|
||||
server.join();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue