Skip to content

Commit

Permalink
Enhanced video loading and poster management, updated Webpack and pac…
Browse files Browse the repository at this point in the history
…kage.json with new packages, and rebuilt Video API for custom experiences and big video
  • Loading branch information
rbi-aap committed Sep 10, 2024
1 parent a1d2243 commit 8d7a608
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 46 deletions.
Binary file added assets/images/poster_default.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 64 additions & 28 deletions assets/ui/components/ArticleBodyHtml.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {formatHTML} from 'utils';
import {connect} from 'react-redux';
import {selectCopy} from '../../wire/actions';
import DOMPurify from 'dompurify';

const fallbackDefault = 'https://storage.googleapis.com/pw-prod-aap-website-bkt/test/aap_poster_default.jpg';
// import fallbackDefault from 'images/poster_default.jpg'
const fallbackDefault = '/static/poster_default.jpg';

class ArticleBodyHtml extends React.PureComponent {
constructor(props) {
Expand Down Expand Up @@ -158,8 +158,9 @@ class ArticleBodyHtml extends React.PureComponent {
document.body.removeChild(script);
};

script.onerrror = (error) => {
throw new URIError('The script ' + error.target.src + 'didn\'t load.');
script.onerror = (error) => {
console.error('Script load error:', error);
throw new URIError('The script ' + error.target.src + ' didn\'t load.');
};

document.body.appendChild(script);
Expand Down Expand Up @@ -230,28 +231,74 @@ class ArticleBodyHtml extends React.PureComponent {
if (!videoSrc || !videoSrc.startsWith('/assets/')) {
return;
}


const loadHandler = () => {
if (player.media.videoWidth === 0 && player.media.videoHeight === 0) {
if (!player.poster) {
player.poster = fallbackDefault;
// eslint-disable-next-line no-console
console.log('Initial dimensions:', player.media.videoWidth, player.media.videoHeight);
const checkVideoContent = () => {
if (player.media.videoWidth > 0 && player.media.videoHeight > 0) {
const canvas = document.createElement('canvas');
canvas.width = player.media.videoWidth;
canvas.height = player.media.videoHeight;
const ctx = canvas.getContext('2d');

ctx.drawImage(player.media, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// loop for none blank pixel
let stepSize = 4; // Adjust the step size as needed
for (let i = 0; i < data.length; i += stepSize * 4) {
if (data[i] > 0 || data[i + 1] > 0 || data[i + 2] > 0) {

// eslint-disable-next-line no-console
console.log('Pixel content detected, poster not needed');
return true;
}
}
}
return false;
};

const attemptContentCheck = () => {
if (checkVideoContent()) {
player.poster = null;
// eslint-disable-next-line no-console
console.log('Pixel content detected, poster removed');
return true;
}
} else {
const isFirstFrameBlack = player.media.videoWidth === 1920 && player.media.videoHeight === 1080;
if (!isFirstFrameBlack)
if (!player.poster) {
return false;
};

let attemptCount = 0;
const maxAttempts = 2;
const checkInterval = setInterval(() => {
if (attemptContentCheck() || attemptCount >= maxAttempts) {
clearInterval(checkInterval);
player.off('loadeddata', loadHandler);

if (attemptCount >= maxAttempts) {
console.warn('Setting fallback poster');
player.poster = fallbackDefault;
}
}
player.off('loadeddata', loadHandler);
}
attemptCount++;
}, 500);
};

player.on('error', (error) => {
console.error('Error details:', {
message: error.message,
code: error.code,
type: error.type,
target: error.target,
currentTarget: error.currentTarget,
originalTarget: error.originalTarget,
error: error.error
});
player.poster = fallbackDefault;
});
player.on('loadeddata', loadHandler);

}


_getBodyHTML(bodyHtml) {
return !bodyHtml ?
null :
Expand All @@ -261,43 +308,34 @@ class ArticleBodyHtml extends React.PureComponent {
_updateImageEmbedSources(html) {
const item = this.props.item;

// Get the list of Original Rendition IDs for all Image Associations
const imageEmbedOriginalIds = Object
.keys(item.associations || {})
.filter((key) => key.startsWith('editor_'))
.map((key) => get(item.associations[key], 'renditions.original.media'))
.filter((value) => value);

if (!imageEmbedOriginalIds.length) {
// This item has no Image Embeds
// return the supplied html as-is
return html;
}

// Create a DOM node tree from the supplied html
// We can then efficiently find and update the image sources
const container = document.createElement('div');
let imageSourcesUpdated = false;

container.innerHTML = html;
container
.querySelectorAll('img,video,audio')
.forEach((imageTag) => {
// Using the tag's `src` attribute, find the Original Rendition's ID
const originalMediaId = imageEmbedOriginalIds.find((mediaId) => (
!imageTag.src.startsWith('/assets/') &&
imageTag.src.includes(mediaId))
);

if (originalMediaId) {
// We now have the Original Rendition's ID
// Use that to update the `src` attribute to use Newshub's Web API
imageSourcesUpdated = true;
imageTag.src = `/assets/${originalMediaId}`;
}
});

// Find all Audio and Video tags and mark them up for the player
container.querySelectorAll('video, audio')
.forEach((vTag) => {
vTag.classList.add('js-player');
Expand All @@ -314,7 +352,6 @@ class ArticleBodyHtml extends React.PureComponent {
}
imageSourcesUpdated = true;
});
// If Image tags were not updated, then return the supplied html as-is
return imageSourcesUpdated ?
container.innerHTML :
html;
Expand All @@ -326,7 +363,6 @@ class ArticleBodyHtml extends React.PureComponent {
if (target && target.tagName === 'A' && this.isLinkExternal(target.href)) {
event.preventDefault();
event.stopPropagation();

const nextWindow = window.open(target.href, '_blank', 'noopener');

if (nextWindow) {
Expand Down
Binary file added newsroom/static/poster_default.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 81 additions & 15 deletions newsroom/upload.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@

import flask
import newsroom
import bson.errors

from werkzeug.wsgi import wrap_file
from werkzeug.http import parse_range_header
from werkzeug.utils import secure_filename
from flask import request, url_for, current_app as newsroom_app
from superdesk.upload import upload_url as _upload_url
from superdesk import get_resource_service
from newsroom.decorator import login_required


cache_for = 3600 * 24 * 7 # 7 days cache
ASSETS_RESOURCE = 'upload'
blueprint = flask.Blueprint(ASSETS_RESOURCE, __name__)


class MediaFileLoader:
_loaded_files = {}

@classmethod
def get_media_file(cls, media_id):
if media_id in cls._loaded_files:
return cls._loaded_files[media_id]

media_file = flask.current_app.media.get(media_id, ASSETS_RESOURCE)

if media_file and 'video' in media_file.content_type:
cls._loaded_files[media_id] = media_file

return media_file


def get_file(key):
file = request.files.get(key)
if file:
Expand All @@ -27,35 +42,86 @@ def get_file(key):
@blueprint.route('/assets/<path:media_id>', methods=['GET'])
@login_required
def get_upload(media_id):
is_safari = ('Safari' in request.headers.get('User-Agent', '') and 'Chrome'
not in request.headers.get('User-Agent', ''))
try:
media_file = flask.current_app.media.get(media_id, ASSETS_RESOURCE)
if is_safari:
media_file = flask.current_app.media.get(media_id, ASSETS_RESOURCE)
else:
media_file = MediaFileLoader.get_media_file(media_id)
except bson.errors.InvalidId:
media_file = None
if not media_file:
flask.abort(404)

data = wrap_file(flask.request.environ, media_file, buffer_size=1024 * 256)
response = flask.current_app.response_class(
data,
mimetype=media_file.content_type,
direct_passthrough=True)
response.content_length = media_file.length
flask.abort(404, description="File not found")

file_size = media_file.length
content_type = media_file.content_type or 'application/octet-stream'
range_header = request.headers.get('Range')
if not is_safari and range_header:
try:
ranges = parse_range_header(range_header)
if ranges and len(ranges.ranges) == 1:
start, end = ranges.ranges[0]
if start is None:
flask.abort(416, description="Invalid range header")
if end is None or end >= file_size:
end = file_size - 1
length = end - start + 1

def range_generate():
media_file.seek(start)
remaining = length
chunk_size = 8192
while remaining:
chunk = media_file.read(min(chunk_size, remaining))
if not chunk:
break
remaining -= len(chunk)
yield chunk

response = flask.Response(
flask.stream_with_context(range_generate()),
206,
mimetype=content_type,
direct_passthrough=True,
)
response.headers.add('Content-Range', f'bytes {start}-{end}/{file_size}')
response.headers.add('Accept-Ranges', 'bytes')
response.headers.add('Content-Length', str(length))
else:
flask.abort(416, description="Requested range not satisfiable")
except ValueError:
flask.abort(400, description="Invalid range header")
else:
data = wrap_file(flask.request.environ, media_file, buffer_size=1024 * 256)
response = flask.current_app.response_class(
data,
mimetype=media_file.content_type,
direct_passthrough=True)
response.content_length = media_file.length

response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response.headers.pop('Content-Disposition', None)
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.last_modified = media_file.upload_date
response.set_etag(media_file.md5)
response.cache_control.max_age = cache_for
response.cache_control.s_max_age = cache_for
response.cache_control.public = True
response.make_conditional(flask.request)

if flask.request.args.get('filename'):
response.headers['Content-Type'] = media_file.content_type
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % flask.request.args['filename']
if request.args.get('filename'):
response.headers['Content-Disposition'] = f'attachment; filename="{request.args["filename"]}"'
else:
response.headers['Content-Disposition'] = 'inline'

item_id = request.args.get('item_id')
if item_id:
get_resource_service('history').log_media_download(item_id, media_id)
try:
get_resource_service('history').log_media_download(item_id, media_id)
except Exception as e:
newsroom_app.logger.error(f"Error logging media download: {str(e)}")

return response

Expand Down
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"eslint": "^4.8.0",
"eslint-plugin-react": "^7.3.0",
"expect": "^21.1.0",
"file-loader": "^1.1.11",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-jasmine": "^1.1.0",
Expand Down
Loading

0 comments on commit 8d7a608

Please sign in to comment.