Skip to content

Jellyfin’s dropAllAnimations #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ When creating an instance of SubtitleOctopus, you can set the following options:
- `maxRenderHeight`: The maximum rendering height of the subtitles canvas.
Beyond this subtitles will be upscaled by the browser.
(Default: `0` - no limit)
- `dropAllAnimations`: If set to true, attempt to discard all animated tags.
Enabling this may severly mangle complex subtitles and
should only be considered as an last ditch effort of uncertain success
for hardware otherwise incapable of displaing anything.
Will not reliably work with manually edited or allocated events.
(Default: `false` - do nothing)

### Rendering Modes
#### JS Blending
Expand Down
157 changes: 153 additions & 4 deletions src/SubtitleOctopus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,137 @@ typedef struct RenderBlendResult {
unsigned char* image;
} RenderBlendResult;

/**
* \brief Overwrite tag with whitespace to nullify its effect
* Boundaries are inclusive at both ends.
*/
static void _remove_tag(char *begin, char *end) {
if (end < begin)
return;
memset(begin, ' ', end - begin + 1);
}

/**
* \param begin point to the first character of the tag name (after backslash)
* \param end last character that can be read; at least the name itself
and the following character if any must be included
* \return true if tag may cause animations, false if it will definitely not
*/
static bool _is_animated_tag(char *begin, char *end) {
if (end <= begin)
return false;

size_t length = end - begin + 1;

#define check_simple_tag(tag) (sizeof(tag)-1 < length && !strncmp(begin, tag, sizeof(tag)-1))
#define check_complex_tag(tag) (check_simple_tag(tag) && (begin[sizeof(tag)-1] == '(' \
|| begin[sizeof(tag)-1] == ' ' || begin[sizeof(tag)-1] == '\t'))
switch (begin[0]) {
case 'k': //-fallthrough
case 'K':
// Karaoke: k, kf, ko, K and kt ; no other valid ASS-tag starts with k/K
return true;
case 't':
// Animated transform: no other valid tag begins with t
// non-nested t-tags have to be complex tags even in single argument
// form, but nested t-tags (which act like independent t-tags) are allowed to be
// simple-tags without parentheses due to VSF-parsing quirk.
// Since all valid simple t-tags require the existence of a complex t-tag, we only check for complex tags
// to avoid false positives from invalid simple t-tags. This makes animation-dropping somewhat incorrect
// but as animation detection remains accurate, we consider this to be "good enough"
return check_complex_tag("t");
case 'm':
// Movement: complex tag; again no other valid tag begins with m
// but ensure it's complex just to be sure
return check_complex_tag("move");
case 'f':
// Fade: \fad and Fade (complex): \fade; both complex
// there are several other valid tags beginning with f
return check_complex_tag("fad") || check_complex_tag("fade");
}

return false;
#undef check_complex_tag
#undef check_simple_tag
}

/**
* \param start First character after { (optionally spaces can be dropped)
* \param end Last character before } (optionally spaces can be dropped)
* \param drop_animations If true animation tags will be discarded
* \return true if after processing the event may contain animations
(i.e. when dropping animations this is always false)
*/
static bool _is_block_animated(char *start, char *end, bool drop_animations)
{
char *tag_start = NULL; // points to beginning backslash
for (char *p = start; p <= end; p++) {
if (*p == '\\') {
// It is safe to go one before and beyond unconditionally
// because the text passed in must be surronded by { }
if (tag_start && _is_animated_tag(tag_start + 1, p - 1)) {
if (!drop_animations)
return true;
// For \t transforms this will assume the final state
_remove_tag(tag_start, p - 1);
}
tag_start = p;
}
}

if (tag_start && _is_animated_tag(tag_start + 1, end)) {
if (!drop_animations)
return true;
_remove_tag(tag_start, end);
}

return false;
}

/**
* \param event ASS event to be processed
* \param drop_animations If true animation tags will be discarded
* \return true if after processing the event may contain animations
(i.e. when dropping animations this is always false)
*/
static bool _is_event_animated(ASS_Event *event, bool drop_animations) {
// Event is animated if it has an Effect or animated override tags
if (event->Effect && event->Effect[0] != '\0') {
if (!drop_animations) return 1;
event->Effect[0] = '\0';
}

// Search for override blocks
// Only closed {...}-blocks are parsed by VSFilters and libass
char *block_start = NULL; // points to opening {
for (char *p = event->Text; *p != '\0'; p++) {
switch (*p) {
case '{':
// Escaping the opening curly bracket to not start an override block is
// a VSFilter-incompatible libass extension. But we only use libass, so...
if (!block_start && (p == event->Text || *(p-1) != '\\'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the escaping be parsed with the boolean flag?
For something like bla-bla \\{\pos(100,200)}.
libass doesn't seem to escape the backslash either: 1\\N2 gets a new line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand what your suggesting with a “boolean flag”. Can you elaborate?
Also there aren't any backslashes being escaped being checked, but backslashes escaping curly brackets (working only in libass).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that the condition only checks the previous backslash, but there can be another one before it. In this case something like if (*p == '\\') escaped = !escaped; is required. But since libass (SSA/ASS standard) has no escaping logic, the current implementation is fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, then it seems this is ready to be merged.

block_start = p;
break;
case '}':
if (block_start && p - block_start > 2
&& _is_block_animated(block_start + 1, p - 1, drop_animations))
return true;
block_start = NULL;
break;
default:
break;
}
}

return false;
}

class SubtitleOctopus {
private:
ReusableBuffer2D m_blend;
RenderBlendResult m_blendResult;
bool drop_animations;
int scanned_events; // next unscanned event index
public:
ASS_Library* ass_library;
ASS_Renderer* ass_renderer;
Expand All @@ -124,12 +254,33 @@ class SubtitleOctopus {
track = NULL;
canvas_w = 0;
canvas_h = 0;
drop_animations = false;
scanned_events = 0;
}

void setLogLevel(int level) {
log_level = level;
}

void setDropAnimations(int value) {
drop_animations = !!value;
if (drop_animations)
scanAnimations(scanned_events);
}

/*
* \brief Scan events starting at index i for animations
* and discard animated tags when found.
* Note that once animated tags were dropped they cannot be restored.
* Updates the class member scanned_events to last scanned index.
*/
void scanAnimations(int i) {
for (; i < track->n_events; i++) {
_is_event_animated(track->events + i, drop_animations);
}
scanned_events = i;
}

void initLibrary(int frame_w, int frame_h) {
ass_library = ass_library_init();
if (!ass_library) {
Expand Down Expand Up @@ -159,6 +310,7 @@ class SubtitleOctopus {
fprintf(stderr, "jso: Failed to start a track\n");
exit(4);
}
scanAnimations(0);
}

void createTrackMem(char *buf, unsigned long bufsize) {
Expand All @@ -168,6 +320,7 @@ class SubtitleOctopus {
fprintf(stderr, "jso: Failed to start a track\n");
exit(4);
}
scanAnimations(0);
}

void removeTrack() {
Expand Down Expand Up @@ -362,10 +515,6 @@ class SubtitleOctopus {
m_blendResult.image = (unsigned char*)result;
return &m_blendResult;
}

private:
ReusableBuffer2D m_blend;
RenderBlendResult m_blendResult;
};

int main(int argc, char *argv[]) { return 0; }
Expand Down
1 change: 1 addition & 0 deletions src/SubtitleOctopus.idl
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ interface SubtitleOctopus {
attribute ASS_Renderer ass_renderer;
attribute ASS_Library ass_library;
void setLogLevel(long level);
void setDropAnimations(long value);
void initLibrary(long frame_w, long frame_h);
void createTrack(DOMString subfile);
void createTrackMem(DOMString buf, unsigned long bufsize);
Expand Down
2 changes: 2 additions & 0 deletions src/post-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ self.nextIsRaf = false;
self.lastCurrentTimeReceivedAt = Date.now();
self.targetFps = 24;
self.libassMemoryLimit = 0; // in MiB
self.dropAllAnimations = false;

self.width = 0;
self.height = 0;
Expand Down Expand Up @@ -565,6 +566,7 @@ function onMessageFromMainEmscriptenThread(message) {
self.targetFps = message.data.targetFps || self.targetFps;
self.libassMemoryLimit = message.data.libassMemoryLimit || self.libassMemoryLimit;
self.libassGlyphLimit = message.data.libassGlyphLimit || 0;
self.dropAllAnimations = !!message.data.dropAllAnimations || self.dropAllAnimations;
removeRunDependency('worker-init');
postMessage({
target: "ready",
Expand Down
1 change: 1 addition & 0 deletions src/pre-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Module['onRuntimeInitialized'] = function () {
self.blendH = Module._malloc(4);

self.octObj.initLibrary(screen.width, screen.height);
self.octObj.setDropAnimations(self.dropAllAnimations);
self.octObj.createTrack("/sub.ass");
self.ass_track = self.octObj.track;
self.ass_library = self.octObj.ass_library;
Expand Down
4 changes: 3 additions & 1 deletion src/subtitles-octopus.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var SubtitlesOctopus = function (options) {
self.prescaleFactor = options.prescaleFactor || 1.0;
self.prescaleHeightLimit = options.prescaleHeightLimit || 1080;
self.maxRenderHeight = options.maxRenderHeight || 0; // 0 - no limit
self.dropAllAnimations = options.dropAllAnimations || false; // attempt to remove all animations as a last ditch effort for displaying on weak hardware; may severly mangle subtitles if enabled
self.isOurCanvas = false; // (internal) we created canvas and manage it
self.video = options.video; // HTML video element (optional if canvas specified)
self.canvasParent = null; // (internal) HTML canvas parent element
Expand Down Expand Up @@ -113,7 +114,8 @@ var SubtitlesOctopus = function (options) {
debug: self.debug,
targetFps: self.targetFps,
libassMemoryLimit: self.libassMemoryLimit,
libassGlyphLimit: self.libassGlyphLimit
libassGlyphLimit: self.libassGlyphLimit,
dropAllAnimations: self.dropAllAnimations
});
};

Expand Down