diff --git a/api/go.mod b/api/go.mod
index a6dddcd..436aadd 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -33,5 +33,6 @@ require (
 	golang.org/x/image v0.0.0-20200618115811-c13761719519
 	golang.org/x/mod v0.3.0 // indirect
 	golang.org/x/tools v0.0.0-20200622192924-4fd1c64487bf // indirect
+	gopkg.in/vansante/go-ffprobe.v2 v2.0.2
 	gopkg.in/yaml.v2 v2.3.0 // indirect
 )
diff --git a/api/go.sum b/api/go.sum
index d6f835e..59b9de4 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -195,6 +195,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/vansante/go-ffprobe.v2 v2.0.2 h1:DdxSfFnlqeawPIVbIQEI6LR6OQHQNR7tNgWb2mWuC4w=
+gopkg.in/vansante/go-ffprobe.v2 v2.0.2/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
diff --git a/api/scanner/encode_photo.go b/api/scanner/encode_photo.go
index 3339d6b..9298122 100644
--- a/api/scanner/encode_photo.go
+++ b/api/scanner/encode_photo.go
@@ -10,6 +10,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/viktorstrate/photoview/api/graphql/models"
 	"github.com/viktorstrate/photoview/api/utils"
+	"gopkg.in/vansante/go-ffprobe.v2"
 )
 
 type PhotoDimensions struct {
@@ -43,12 +44,13 @@ func (dimensions *PhotoDimensions) ThumbnailScale() PhotoDimensions {
 	}
 }
 
-// EncodeImageData is used to easily decode image data, with a cache so expensive operations are not repeated
-type EncodeImageData struct {
+// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated
+type EncodeMediaData struct {
 	media           *models.Media
 	_photoImage     image.Image
 	_thumbnailImage image.Image
 	_contentType    *MediaType
+	_videoMetadata  *ffprobe.Stream
 }
 
 func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error {
@@ -85,7 +87,7 @@ func GetPhotoDimensions(imagePath string) (*PhotoDimensions, error) {
 }
 
 // ContentType reads the image to determine its content type
-func (img *EncodeImageData) ContentType() (*MediaType, error) {
+func (img *EncodeMediaData) ContentType() (*MediaType, error) {
 	if img._contentType != nil {
 		return img._contentType, nil
 	}
@@ -99,7 +101,7 @@ func (img *EncodeImageData) ContentType() (*MediaType, error) {
 	return imgType, nil
 }
 
-func (img *EncodeImageData) EncodeHighRes(tx *sql.Tx, outputPath string) error {
+func (img *EncodeMediaData) EncodeHighRes(tx *sql.Tx, outputPath string) error {
 	contentType, err := img.ContentType()
 	if err != nil {
 		return err
@@ -154,7 +156,7 @@ func EncodeThumbnail(inputPath string, outputPath string) (*PhotoDimensions, err
 }
 
 // PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
-func (img *EncodeImageData) photoImage(tx *sql.Tx) (image.Image, error) {
+func (img *EncodeMediaData) photoImage(tx *sql.Tx) (image.Image, error) {
 	if img._photoImage != nil {
 		return img._photoImage, nil
 	}
diff --git a/api/scanner/executable_worker.go b/api/scanner/executable_worker.go
index ab014d2..dc02441 100644
--- a/api/scanner/executable_worker.go
+++ b/api/scanner/executable_worker.go
@@ -91,6 +91,7 @@ func (worker *FfmpegWorker) EncodeMp4(inputPath string, outputPath string) error
 		inputPath,
 		"-vcodec", "h264",
 		"-acodec", "aac",
+		"-vf", "scale='min(1080,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease",
 		outputPath,
 	}
 
diff --git a/api/scanner/photo_type.go b/api/scanner/photo_type.go
index d9d3586..d2aca12 100644
--- a/api/scanner/photo_type.go
+++ b/api/scanner/photo_type.go
@@ -159,18 +159,18 @@ var fileExtensions = map[string]MediaType{
 	".fff": TypeFFF,
 
 	// Video formats
-	"mp4":  TypeMP4,
-	"m4v":  TypeMP4,
-	"mpeg": TypeMPEG,
-	"3gp":  Type3GP,
-	"3g2":  Type3G2,
-	"ogv":  TypeOGV,
-	"wmv":  TypeWMV,
-	"avi":  TypeAVI,
-	"webm": TypeWEBM,
-	"mov":  TypeMOV,
-	"qt":   TypeMOV,
-	"ts":   TypeTS,
+	".mp4":  TypeMP4,
+	".m4v":  TypeMP4,
+	".mpeg": TypeMPEG,
+	".3gp":  Type3GP,
+	".3g2":  Type3G2,
+	".ogv":  TypeOGV,
+	".wmv":  TypeWMV,
+	".avi":  TypeAVI,
+	".webm": TypeWEBM,
+	".mov":  TypeMOV,
+	".qt":   TypeMOV,
+	".ts":   TypeTS,
 }
 
 func (imgType *MediaType) isRaw() bool {
diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go
index a756408..7c63362 100644
--- a/api/scanner/process_photo.go
+++ b/api/scanner/process_photo.go
@@ -44,7 +44,7 @@ func makePhotoURLChecker(tx *sql.Tx, mediaID int) (func(purpose models.MediaPurp
 }
 
 func ProcessMedia(tx *sql.Tx, media *models.Media) (bool, error) {
-	imageData := EncodeImageData{
+	imageData := EncodeMediaData{
 		media: media,
 	}
 
@@ -66,7 +66,7 @@ func ProcessMedia(tx *sql.Tx, media *models.Media) (bool, error) {
 	}
 }
 
-func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string) (bool, error) {
+func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string) (bool, error) {
 
 	photo := imageData.media
 
@@ -131,8 +131,7 @@ func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string
 			_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
 				photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
 			if err != nil {
-				log.Printf("Could not insert highres media url: %d, %s\n", photo.MediaID, path.Base(photo.Path))
-				return false, err
+				return false, errors.Wrapf(err, "could not insert highres media url (%d, %s)", photo.MediaID, photo.Title)
 			}
 		}
 	} else {
@@ -238,7 +237,7 @@ func makeMediaCacheDir(photo *models.Media) (*string, error) {
 	return &photoCachePath, nil
 }
 
-func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeImageData, photoDimensions *PhotoDimensions) error {
+func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeMediaData, photoDimensions *PhotoDimensions) error {
 	photoName := path.Base(photo.Path)
 	photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
 	photoBaseExt := path.Ext(photoName)
diff --git a/api/scanner/process_video.go b/api/scanner/process_video.go
index 4820ee4..1465fb6 100644
--- a/api/scanner/process_video.go
+++ b/api/scanner/process_video.go
@@ -1,15 +1,22 @@
 package scanner
 
 import (
+	"context"
 	"database/sql"
+	"fmt"
 	"log"
+	"path"
+	"strings"
+	"time"
 
 	"github.com/pkg/errors"
 	"github.com/viktorstrate/photoview/api/graphql/models"
+	"github.com/viktorstrate/photoview/api/utils"
+	"gopkg.in/vansante/go-ffprobe.v2"
 )
 
-func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string) (bool, error) {
-	video := imageData.media
+func processVideo(tx *sql.Tx, mediaData *EncodeMediaData, videoCachePath *string) (bool, error) {
+	video := mediaData.media
 	didProcess := false
 
 	log.Printf("Processing video: %s", video.Path)
@@ -25,10 +32,63 @@ func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string
 	}
 
 	if videoWebURL == nil {
-		// TODO: Process web video
+		web_video_name := fmt.Sprintf("web_video_%s_%s", path.Base(video.Path), utils.GenerateToken())
+		web_video_name = strings.ReplaceAll(web_video_name, ".", "_")
+		web_video_name = strings.ReplaceAll(web_video_name, " ", "_")
+		web_video_name = web_video_name + ".mp4"
+
+		webVideoPath := path.Join(*videoCachePath, web_video_name)
+
+		err = FfmpegCli.EncodeMp4(video.Path, webVideoPath)
+		if err != nil {
+			return false, errors.Wrapf(err, "could not encode mp4 video (%s)", video.Path)
+		}
+
+		webMetadata, err := readVideoMetadata(webVideoPath)
+		if err != nil {
+			return false, errors.Wrapf(err, "failed to read metadata for encoded web-video (%s)", video.Title)
+		}
+
+		_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
+			video.MediaID, web_video_name, webMetadata.Width, webMetadata.Height, models.VideoWeb, "video/mp4")
+		if err != nil {
+			return false, errors.Wrapf(err, "failed to insert encoded web-video into database (%s)", video.Title)
+		}
 	}
 
 	// TODO: Process video thumbnail
 
 	return didProcess, nil
 }
+
+func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.Stream, error) {
+
+	if enc._videoMetadata != nil {
+		return enc._videoMetadata, nil
+	}
+
+	metadata, err := readVideoMetadata(enc.media.Path)
+	if err != nil {
+		return nil, err
+	}
+
+	enc._videoMetadata = metadata
+	return metadata, nil
+}
+
+func readVideoMetadata(videoPath string) (*ffprobe.Stream, error) {
+	ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancelFn()
+
+	data, err := ffprobe.ProbeURL(ctx, videoPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "could not read video metadata (%s)", path.Base(videoPath))
+	}
+
+	stream := data.FirstVideoStream()
+	if stream == nil {
+		return nil, errors.Wrapf(err, "could not get stream from file metadata (%s)", path.Base(videoPath))
+	}
+
+	return stream, nil
+}
