Skip to content

Commit

Permalink
Render heatmaps on scene thumbnails in DeoVR (#464)
Browse files Browse the repository at this point in the history
  • Loading branch information
crwxaj authored Jun 7, 2021
1 parent 81ca73c commit 4cc29c4
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 53 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/bregydoc/gtranslate v0.0.0-20200913051839-1bd07f6c1fc5
github.com/creasty/defaults v1.5.1
github.com/darwayne/go-timecode v1.1.0
github.com/disintegration/imaging v1.6.0
github.com/djherbis/times v1.2.0
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0
Expand Down
16 changes: 13 additions & 3 deletions pkg/api/deovr.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,10 +337,14 @@ func (i DeoVRResource) getDeoScene(req *restful.Request, resp *restful.Response)
screenType = "sphere"
}

var title = scene.Title
title := scene.Title
thumbnailURL := session.DeoRequestHost + "/img/700x/" + strings.Replace(scene.CoverURL, "://", ":/", -1)

if scene.IsScripted {
title = scene.GetFunscriptTitle()
if config.Config.Interfaces.DeoVR.RenderHeatmaps {
thumbnailURL = session.DeoRequestHost + "/imghm/" + fmt.Sprint(scene.ID) + "/" + strings.Replace(scene.CoverURL, "://", ":/", -1)
}
}

deoScene := DeoScene{
Expand All @@ -354,7 +358,7 @@ func (i DeoVRResource) getDeoScene(req *restful.Request, resp *restful.Response)
RatingAvg: scene.StarRating,
FullVideoReady: true,
FullAccess: true,
ThumbnailURL: session.DeoRequestHost + "/img/700x/" + strings.Replace(scene.CoverURL, "://", ":/", -1),
ThumbnailURL: thumbnailURL,
StereoMode: stereoMode,
Is3D: true,
ScreenType: screenType,
Expand Down Expand Up @@ -426,10 +430,16 @@ func scenesToDeoList(req *restful.Request, scenes []models.Scene) []DeoListItem

list := make([]DeoListItem, 0)
for i := range scenes {
thumbnailURL := fmt.Sprintf("%v/img/700x/%v", session.DeoRequestHost, strings.Replace(scenes[i].CoverURL, "://", ":/", -1))

if config.Config.Interfaces.DeoVR.RenderHeatmaps && scenes[i].IsScripted {
thumbnailURL = fmt.Sprintf("%v/imghm/%d/%v", session.DeoRequestHost, scenes[i].ID, strings.Replace(scenes[i].CoverURL, "://", ":/", -1))
}

item := DeoListItem{
Title: scenes[i].Title,
VideoLength: scenes[i].Duration * 60,
ThumbnailURL: fmt.Sprintf("%v/img/700x/%v", session.DeoRequestHost, strings.Replace(scenes[i].CoverURL, "://", ":/", -1)),
ThumbnailURL: thumbnailURL,
VideoURL: fmt.Sprintf("%v/deovr/%v", session.DeoRequestHost, scenes[i].ID),
}
list = append(list, item)
Expand Down
12 changes: 7 additions & 5 deletions pkg/api/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ type RequestSaveOptionsDLNA struct {
}

type RequestSaveOptionsDeoVR struct {
Enabled bool `json:"enabled"`
AuthEnabled bool `json:"auth_enabled"`
Username string `json:"username"`
Password string `json:"password"`
RemoteEnabled bool `json:"remote_enabled"`
Enabled bool `json:"enabled"`
AuthEnabled bool `json:"auth_enabled"`
Username string `json:"username"`
Password string `json:"password"`
RemoteEnabled bool `json:"remote_enabled"`
RenderHeatmaps bool `json:"render_heatmaps"`
}

type RequestSaveOptionsPreviews struct {
Expand Down Expand Up @@ -229,6 +230,7 @@ func (i ConfigResource) saveOptionsDeoVR(req *restful.Request, resp *restful.Res

config.Config.Interfaces.DeoVR.Enabled = r.Enabled
config.Config.Interfaces.DeoVR.AuthEnabled = r.AuthEnabled
config.Config.Interfaces.DeoVR.RenderHeatmaps = r.RenderHeatmaps
config.Config.Interfaces.DeoVR.RemoteEnabled = r.RemoteEnabled
config.Config.Interfaces.DeoVR.Username = r.Username
if r.Password != config.Config.Interfaces.DeoVR.Password && r.Password != "" {
Expand Down
11 changes: 6 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ type ObjectConfig struct {
AllowedIP []string `default:"[]" json:"allowedIp"`
} `json:"dlna"`
DeoVR struct {
Enabled bool `default:"true" json:"enabled"`
AuthEnabled bool `default:"false" json:"auth_enabled"`
RemoteEnabled bool `default:"false" json:"remote_enabled"`
Username string `default:"" json:"username"`
Password string `default:"" json:"password"`
Enabled bool `default:"true" json:"enabled"`
AuthEnabled bool `default:"false" json:"auth_enabled"`
RenderHeatmaps bool `default:"false" json:"render_heatmaps"`
RemoteEnabled bool `default:"false" json:"remote_enabled"`
Username string `default:"" json:"username"`
Password string `default:"" json:"password"`
} `json:"deovr"`
} `json:"interfaces"`
Library struct {
Expand Down
194 changes: 194 additions & 0 deletions pkg/server/heatmapproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package server

import (
"bytes"
"fmt"
"image"
"image/draw"
"image/jpeg"
"image/png"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/disintegration/imaging"
"github.com/xbapps/xbvr/pkg/common"
"github.com/xbapps/xbvr/pkg/models"
"willnorris.com/go/imageproxy"
)

const thumbnailWidth = 700
const thumbnailHeight = 420
const heatmapHeight = 10
const heatmapMargin = 3

type BufferResponseWriter struct {
header http.Header
statusCode int
buf *bytes.Buffer
}

func (myrw *BufferResponseWriter) Write(p []byte) (int, error) {
return myrw.buf.Write(p)
}

func (w *BufferResponseWriter) Header() http.Header {
return w.header
}

func (w *BufferResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}

type HeatmapThumbnailProxy struct {
ImageProxy *imageproxy.Proxy
Cache imageproxy.Cache
}

func NewHeatmapThumbnailProxy(imageproxy *imageproxy.Proxy, cache imageproxy.Cache) *HeatmapThumbnailProxy {
proxy := &HeatmapThumbnailProxy{
ImageProxy: imageproxy,
Cache: cache,
}
return proxy
}

func getScriptFileId(urlpart string) (uint, error) {
sceneId, err := strconv.Atoi(urlpart)
if err != nil {
return 0, err
}

var scene models.Scene
err = scene.GetIfExistByPK(uint(sceneId))
if err != nil {
return 0, err
}
scriptfiles, err := scene.GetScriptFiles()
if err != nil || len(scriptfiles) < 1 {
return 0, fmt.Errorf("scene %d has no script files", sceneId)
}
return scriptfiles[0].ID, nil
}

func getHeatmapImageForScene(fileId uint) (image.Image, error) {

heatmapFilename := filepath.Join(common.ScriptHeatmapDir, fmt.Sprintf("heatmap-%d.png", fileId))
heatmapFile, err := os.Open(heatmapFilename)
if err != nil {
return nil, err
}

heatmapImage, err := png.Decode(heatmapFile)
heatmapFile.Close()
if err != nil {
return nil, err
}

return heatmapImage, nil
}

func createHeatmapThumbnail(out *bytes.Buffer, r io.Reader, heatmapImage image.Image) error {
thumbnailImage, err := jpeg.Decode(r)

if err != nil {
return err
}

rect := thumbnailImage.Bounds()
if rect.Dx() != thumbnailWidth || rect.Dy() != thumbnailHeight-heatmapHeight-heatmapMargin {
thumbnailImage = imaging.Fill(thumbnailImage, thumbnailWidth, thumbnailHeight-heatmapHeight-heatmapMargin, imaging.Center, imaging.Linear)
}
heatmapImage = imaging.Resize(heatmapImage, thumbnailWidth, heatmapHeight, imaging.Linear)

canvas := image.NewNRGBA(image.Rect(0, 0, thumbnailWidth, thumbnailHeight))

drawRect := image.Rect(0, 0, thumbnailWidth, thumbnailHeight-heatmapHeight-heatmapMargin)
draw.Draw(canvas, drawRect, thumbnailImage, image.Point{}, draw.Over)
drawRect = image.Rect(0, thumbnailHeight-heatmapHeight, thumbnailWidth, thumbnailHeight)
draw.Draw(canvas, drawRect, heatmapImage, image.Point{}, draw.Over)
jpeg.Encode(out, canvas, &jpeg.Options{Quality: 90})
return nil
}

func (p *HeatmapThumbnailProxy) serveImageproxyResponse(w http.ResponseWriter, r *http.Request, imageURL string) {
proxyURL := "/700x/" + imageURL
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = proxyURL
p.ImageProxy.ServeHTTP(w, r2)
}

func (p *HeatmapThumbnailProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {

parts := strings.SplitN(r.URL.Path, "/", 3)
if len(parts) != 3 {
http.NotFound(w, r)
return
}

imageURL := parts[2]
fileId, err := getScriptFileId(parts[1])
if err != nil {
p.serveImageproxyResponse(w, r, imageURL)
return
}

cacheKey := fmt.Sprintf("%d:%s", fileId, imageURL)
cachedContent, ok := p.Cache.Get(cacheKey)
if ok {
w.Header().Add("Content-Type", "image/jpeg")
w.Header().Add("Content-Length", fmt.Sprint(len(cachedContent)))
if _, err := io.Copy(w, bytes.NewReader(cachedContent)); err != nil {
log.Printf("Failed to send out response: %v", err)
}
return
}

heatmapImage, err := getHeatmapImageForScene(fileId)
if err != nil {
p.serveImageproxyResponse(w, r, imageURL)
return
}

proxyURL := fmt.Sprintf("/%dx%d,jpeg/%s", thumbnailWidth, thumbnailHeight-heatmapHeight-heatmapMargin, imageURL)
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = proxyURL
imageproxyResponseWriter := &BufferResponseWriter{
header: http.Header{},
buf: &bytes.Buffer{},
}
p.ImageProxy.ServeHTTP(imageproxyResponseWriter, r2)

respbody, err := ioutil.ReadAll(imageproxyResponseWriter.buf)
if err == nil {
var output bytes.Buffer
err = createHeatmapThumbnail(&output, bytes.NewReader(respbody), heatmapImage)
if err == nil {
p.Cache.Set(cacheKey, output.Bytes())
w.Header().Add("Content-Type", "image/jpeg")
w.Header().Add("Content-Length", fmt.Sprint(len(output.Bytes())))
if _, err := io.Copy(w, bytes.NewReader(output.Bytes())); err != nil {
log.Printf("Failed to send out response: %v", err)
}
return
}
}
if err != nil {
log.Printf("%v", err)
// serve original response
if _, err := io.Copy(w, bytes.NewReader(respbody)); err != nil {
log.Printf("Failed to send out response: %v", err)
}
}
}
2 changes: 2 additions & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ func StartServer(version, commit, branch, date string) {
p := imageproxy.NewProxy(nil, diskCache(filepath.Join(common.AppDir, "imageproxy")))
p.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"
r.PathPrefix("/img/").Handler(http.StripPrefix("/img", p))
hmp := NewHeatmapThumbnailProxy(p, diskCache(filepath.Join(common.AppDir, "heatmapthumbnailproxy")))
r.PathPrefix("/imghm/").Handler(http.StripPrefix("/imghm", hmp))
r.SkipClean(true)

r.PathPrefix("/").Handler(http.DefaultServeMux)
Expand Down
Loading

0 comments on commit 4cc29c4

Please sign in to comment.