mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
9d2094fe23
145 changed files with 34121 additions and 2954 deletions
|
|
@ -31,7 +31,7 @@ class UITests: XCTestCase {
|
|||
}
|
||||
|
||||
func testSignUp() throws {
|
||||
deleteTestAccount(phone: "9996629999")
|
||||
deleteTestAccount(phone: "9996625296")
|
||||
app.launch()
|
||||
|
||||
// Welcome screen — tap Start Messaging
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ swift_library(
|
|||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl",
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import UIKit
|
|||
|
||||
import Display
|
||||
import AnimationCache
|
||||
import DCTAnimationCacheImpl
|
||||
import SwiftSignalKit
|
||||
import VideoAnimationCache
|
||||
import LottieAnimationCache
|
||||
|
|
@ -50,7 +51,7 @@ public final class ViewController: UIViewController {
|
|||
let basePath = NSTemporaryDirectory() + "/animation-cache"
|
||||
let _ = try? FileManager.default.removeItem(atPath: basePath)
|
||||
let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: basePath), withIntermediateDirectories: true)
|
||||
self.cache = AnimationCacheImpl(basePath: basePath, allocateTempFile: {
|
||||
self.cache = DCTAnimationCacheImpl(basePath: basePath, allocateTempFile: {
|
||||
return basePath + "/\(Int64.random(in: 0 ... Int64.max))"
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import sys
|
|||
import json
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
|
||||
def sha256_file(path):
|
||||
|
|
@ -26,16 +27,55 @@ def init_build(host, token, files, channel):
|
|||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
class ProgressFileReader:
|
||||
def __init__(self, path, size):
|
||||
self._file = open(path, 'rb')
|
||||
self._size = size
|
||||
self._sent = 0
|
||||
self._start_time = time.time()
|
||||
self._last_print = self._start_time
|
||||
|
||||
def __len__(self):
|
||||
return self._size
|
||||
|
||||
def read(self, chunk_size=-1):
|
||||
if chunk_size == -1:
|
||||
chunk_size = self._size
|
||||
data = self._file.read(chunk_size)
|
||||
if not data:
|
||||
elapsed = time.time() - self._start_time
|
||||
speed = self._size / elapsed / 1024 / 1024 if elapsed > 0 else 0
|
||||
print(' 100% - all bytes sent in {:.1f}s ({:.2f} MB/s), waiting for server response...'.format(elapsed, speed))
|
||||
return data
|
||||
self._sent += len(data)
|
||||
now = time.time()
|
||||
if now - self._last_print >= 5:
|
||||
elapsed = now - self._start_time
|
||||
speed = self._sent / elapsed / 1024 / 1024 if elapsed > 0 else 0
|
||||
print(' {:.1f}% ({:.1f} / {:.1f} MB) {:.2f} MB/s'.format(
|
||||
self._sent * 100 / self._size, self._sent / 1024 / 1024, self._size / 1024 / 1024, speed))
|
||||
self._last_print = now
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
self._file.close()
|
||||
|
||||
def upload_file(path, upload_info):
|
||||
url = upload_info.get('url')
|
||||
headers = dict(upload_info.get('headers', {}))
|
||||
|
||||
|
||||
size = os.path.getsize(path)
|
||||
headers['Content-Length'] = str(size)
|
||||
|
||||
print('Uploading', path)
|
||||
with open(path, 'rb') as f:
|
||||
r = requests.put(url, data=f, headers=headers, timeout=900)
|
||||
print('Uploading {} ({:.1f} MB)'.format(path, size / 1024 / 1024))
|
||||
start_time = time.time()
|
||||
|
||||
body = ProgressFileReader(path, size)
|
||||
try:
|
||||
r = requests.put(url, data=body, headers=headers, timeout=900)
|
||||
finally:
|
||||
body.close()
|
||||
print(' Server responded: {} ({:.1f}s total)'.format(r.status_code, time.time() - start_time))
|
||||
if r.status_code != 200:
|
||||
print('Upload failed', r.status_code)
|
||||
print(r.text[:500])
|
||||
|
|
|
|||
|
|
@ -7177,9 +7177,6 @@ private final class ChatListLocationContext {
|
|||
titleContent = NetworkStatusTitle(text: presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus)
|
||||
case let .connecting(proxy):
|
||||
let text = presentationData.strings.State_Connecting
|
||||
/*if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 {*/
|
||||
//text = self.presentationData.strings.State_ConnectingToProxy
|
||||
//}
|
||||
if let proxy = proxy, proxy.hasConnectionIssues {
|
||||
checkProxy = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2722,7 +2722,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
let messageString: NSAttributedString
|
||||
if !messageText.isEmpty && entities.count > 0 {
|
||||
messageString = foldLineBreaks(stringWithAppliedEntities(messageText, entities: entities, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()))
|
||||
let appliedString = stringWithAppliedEntities(messageText, entities: entities, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
||||
messageString = foldLineBreaks(appliedString)
|
||||
} else if spoilers != nil || customEmojiRanges != nil {
|
||||
let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
|
||||
if let spoilers = spoilers {
|
||||
|
|
|
|||
|
|
@ -287,7 +287,9 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
}
|
||||
if !processed {
|
||||
if !message.text.isEmpty {
|
||||
messageText = "📎 \(messageText)"
|
||||
if enableMediaEmoji {
|
||||
messageText = "📎 \(messageText)"
|
||||
}
|
||||
} else {
|
||||
if fileMedia.isAnimatedSticker {
|
||||
messageText = strings.Message_Sticker
|
||||
|
|
|
|||
|
|
@ -1216,11 +1216,11 @@ open class TextNode: ASDisplayNode, TextNodeProtocol {
|
|||
public static let all: RenderContentTypes = [.text, .emoji]
|
||||
}
|
||||
|
||||
final class DrawingParameters: NSObject {
|
||||
public final class DrawingParameters: NSObject {
|
||||
let cachedLayout: TextNodeLayout?
|
||||
let renderContentTypes: RenderContentTypes
|
||||
|
||||
init(cachedLayout: TextNodeLayout?, renderContentTypes: RenderContentTypes) {
|
||||
public init(cachedLayout: TextNodeLayout?, renderContentTypes: RenderContentTypes) {
|
||||
self.cachedLayout = cachedLayout
|
||||
self.renderContentTypes = renderContentTypes
|
||||
|
||||
|
|
@ -1295,7 +1295,7 @@ open class TextNode: ASDisplayNode, TextNodeProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
private static func calculateLayoutV2(
|
||||
public static func calculateLayoutV2(
|
||||
attributedString: NSAttributedString,
|
||||
minimumNumberOfLines: Int,
|
||||
maximumNumberOfLines: Int,
|
||||
|
|
@ -1685,7 +1685,7 @@ open class TextNode: ASDisplayNode, TextNodeProtocol {
|
|||
)
|
||||
}
|
||||
|
||||
static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout {
|
||||
public static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout {
|
||||
guard let attributedString else {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,14 +264,11 @@ static NSData *base64_decode(NSString *str) {
|
|||
|
||||
- (NSString * _Nonnull)serializeToString {
|
||||
NSData *data = [self serialize];
|
||||
if ([data respondsToSelector:@selector(base64EncodedDataWithOptions:)]) {
|
||||
return [[data base64EncodedStringWithOptions:kNilOptions] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return [self.serialize base64Encoding];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
NSString *result = [data base64EncodedStringWithOptions:kNilOptions];
|
||||
result = [result stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
||||
result = [result stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
||||
result = [result stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,28 @@ static bool MTFillRandomBytes(uint8_t *buffer, size_t length) {
|
|||
return SecRandomCopyBytes(kSecRandomDefault, length, buffer) == errSecSuccess;
|
||||
}
|
||||
|
||||
static bool generate_ml_kem_public_key(uint8_t key[1184]) {
|
||||
for (int i = 0; i < 384; i++) {
|
||||
uint32_t randA = 0;
|
||||
uint32_t randB = 0;
|
||||
if (SecRandomCopyBytes(kSecRandomDefault, 4, (uint8_t *)&randA) != errSecSuccess) {
|
||||
return false;
|
||||
}
|
||||
if (SecRandomCopyBytes(kSecRandomDefault, 4, (uint8_t *)&randB) != errSecSuccess) {
|
||||
return false;
|
||||
}
|
||||
uint32_t a = randA % 3329;
|
||||
uint32_t b = randB % 3329;
|
||||
key[i * 3 + 0] = (uint8_t)(a & 0xFF);
|
||||
key[i * 3 + 1] = (uint8_t)((a >> 8) | ((b & 0x0F) << 4));
|
||||
key[i * 3 + 2] = (uint8_t)(b >> 4);
|
||||
}
|
||||
if (SecRandomCopyBytes(kSecRandomDefault, 32, key + 1152) != errSecSuccess) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool MTGenerateGreaseValues(uint8_t grease[8]) {
|
||||
if (!MTFillRandomBytes(grease, 8)) {
|
||||
return false;
|
||||
|
|
@ -137,7 +159,12 @@ typedef enum {
|
|||
HelloGenerationCommandGrease = 5,
|
||||
HelloGenerationCommandKey = 6,
|
||||
HelloGenerationCommandPushLengthPosition = 7,
|
||||
HelloGenerationCommandPopLengthPosition = 8
|
||||
HelloGenerationCommandPopLengthPosition = 8,
|
||||
HelloGenerationCommandMlKemKey = 9,
|
||||
HelloGenerationCommandBeginChoice = 10,
|
||||
HelloGenerationCommandBeginAlternative = 11,
|
||||
HelloGenerationCommandEndAlternative = 12,
|
||||
HelloGenerationCommandEndChoice = 13
|
||||
} HelloGenerationCommand;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -148,7 +175,7 @@ static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *st
|
|||
while (state->position < string.length) {
|
||||
unichar c = [string characterAtIndex:state->position];
|
||||
state->position += 1;
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (c == '\n' || c == '\r' || c == ' ') {
|
||||
continue;
|
||||
} else if (c == 'S') {
|
||||
return HelloGenerationCommandString;
|
||||
|
|
@ -162,10 +189,20 @@ static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *st
|
|||
return HelloGenerationCommandGrease;
|
||||
} else if (c == 'K') {
|
||||
return HelloGenerationCommandKey;
|
||||
} else if (c == 'M') {
|
||||
return HelloGenerationCommandMlKemKey;
|
||||
} else if (c == '[') {
|
||||
return HelloGenerationCommandPushLengthPosition;
|
||||
} else if (c == ']') {
|
||||
return HelloGenerationCommandPopLengthPosition;
|
||||
} else if (c == '<') {
|
||||
return HelloGenerationCommandBeginChoice;
|
||||
} else if (c == '(') {
|
||||
return HelloGenerationCommandBeginAlternative;
|
||||
} else if (c == ')') {
|
||||
return HelloGenerationCommandEndAlternative;
|
||||
} else if (c == '>') {
|
||||
return HelloGenerationCommandEndChoice;
|
||||
} else {
|
||||
return HelloGenerationCommandInvalid;
|
||||
}
|
||||
|
|
@ -187,21 +224,6 @@ static bool parseSpace(NSString *string, HelloParseState *state) {
|
|||
return seenSpace;
|
||||
}
|
||||
|
||||
static bool parseEndlineOrEnd(NSString *string, HelloParseState *state) {
|
||||
while (state->position < string.length) {
|
||||
unichar c = [string characterAtIndex:state->position];
|
||||
if (c == '\n') {
|
||||
state->position += 1;
|
||||
return true;
|
||||
} else if (c == ' ' || c == '\r') {
|
||||
state->position += 1;
|
||||
continue;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseHexByte(unichar c, uint8_t *output) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
|
|
@ -284,13 +306,11 @@ static bool parseIntArgument(NSString *string, HelloParseState *state, int *outp
|
|||
value = value * 10 + (c - '0');
|
||||
hasDigit = true;
|
||||
state->position += 1;
|
||||
} else if (c == ' ') {
|
||||
state->position += 1;
|
||||
} else if (c == '\n') {
|
||||
state->position += 1;
|
||||
break;
|
||||
} else {
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,14 +325,173 @@ static bool parseIntArgument(NSString *string, HelloParseState *state, int *outp
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool executeGenerationCodeRecursive(NSString *code, HelloParseState *state, uint8_t grease[8], NSData *domain, id<EncryptionProvider> provider, NSMutableData *resultData, NSMutableArray<NSNumber *> *lengthStack) {
|
||||
while (true) {
|
||||
HelloGenerationCommand command = parseCommand(code, state);
|
||||
if (command == HelloGenerationCommandInvalid) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case HelloGenerationCommandString: {
|
||||
if (!parseSpace(code, state)) {
|
||||
return false;
|
||||
}
|
||||
NSData *data = parseHexStringArgument(code, state);
|
||||
if (data == nil) {
|
||||
return false;
|
||||
}
|
||||
[resultData appendData:data];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandZero: {
|
||||
if (!parseSpace(code, state)) {
|
||||
return false;
|
||||
}
|
||||
int zeroLength = 0;
|
||||
if (!parseIntArgument(code, state, &zeroLength)) {
|
||||
return false;
|
||||
}
|
||||
if (zeroLength < 0) {
|
||||
return false;
|
||||
}
|
||||
NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength];
|
||||
[resultData appendData:zeros];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandRandom: {
|
||||
if (!parseSpace(code, state)) {
|
||||
return false;
|
||||
}
|
||||
int randomLength = 0;
|
||||
if (!parseIntArgument(code, state, &randomLength)) {
|
||||
return false;
|
||||
}
|
||||
if (randomLength < 0) {
|
||||
return false;
|
||||
}
|
||||
NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength];
|
||||
if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) {
|
||||
return false;
|
||||
}
|
||||
[resultData appendData:randomData];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandDomain: {
|
||||
[resultData appendData:domain];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandGrease: {
|
||||
if (!parseSpace(code, state)) {
|
||||
return false;
|
||||
}
|
||||
int greaseIndex = 0;
|
||||
if (!parseIntArgument(code, state, &greaseIndex)) {
|
||||
return false;
|
||||
}
|
||||
if (greaseIndex < 0 || greaseIndex >= 8) {
|
||||
return false;
|
||||
}
|
||||
uint8_t value = grease[greaseIndex];
|
||||
[resultData appendBytes:&value length:1];
|
||||
[resultData appendBytes:&value length:1];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandKey: {
|
||||
NSMutableData *key = [[NSMutableData alloc] initWithLength:32];
|
||||
generate_public_key((unsigned char *)key.mutableBytes, provider);
|
||||
[resultData appendData:key];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandMlKemKey: {
|
||||
NSMutableData *key = [[NSMutableData alloc] initWithLength:1184];
|
||||
if (!generate_ml_kem_public_key((uint8_t *)key.mutableBytes)) {
|
||||
return false;
|
||||
}
|
||||
[resultData appendData:key];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandPushLengthPosition: {
|
||||
NSUInteger lengthPosition = resultData.length;
|
||||
uint8_t zeroBytes[2] = { 0, 0 };
|
||||
[resultData appendBytes:zeroBytes length:2];
|
||||
[lengthStack addObject:@(lengthPosition)];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandPopLengthPosition: {
|
||||
if (lengthStack.count == 0) {
|
||||
return false;
|
||||
}
|
||||
NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue];
|
||||
[lengthStack removeLastObject];
|
||||
if (resultData.length < position + 2) {
|
||||
return false;
|
||||
}
|
||||
uint16_t blockLength = (uint16_t)(resultData.length - position - 2);
|
||||
((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff);
|
||||
((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff);
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandBeginChoice: {
|
||||
NSMutableArray<NSMutableData *> *alternatives = [[NSMutableArray alloc] init];
|
||||
while (true) {
|
||||
HelloGenerationCommand nextCommand = parseCommand(code, state);
|
||||
if (nextCommand == HelloGenerationCommandEndChoice) {
|
||||
break;
|
||||
}
|
||||
if (nextCommand != HelloGenerationCommandBeginAlternative) {
|
||||
return false;
|
||||
}
|
||||
NSMutableData *altData = [[NSMutableData alloc] init];
|
||||
NSMutableArray<NSNumber *> *altLengthStack = [[NSMutableArray alloc] init];
|
||||
if (!executeGenerationCodeRecursive(code, state, grease, domain, provider, altData, altLengthStack)) {
|
||||
return false;
|
||||
}
|
||||
if (altLengthStack.count != 0) {
|
||||
return false;
|
||||
}
|
||||
[alternatives addObject:altData];
|
||||
}
|
||||
if (alternatives.count == 0) {
|
||||
return false;
|
||||
}
|
||||
uint32_t choiceIndex = arc4random_uniform((uint32_t)alternatives.count);
|
||||
[resultData appendData:alternatives[choiceIndex]];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandEndAlternative: {
|
||||
// signals end of current alternative — return to caller
|
||||
return true;
|
||||
}
|
||||
case HelloGenerationCommandBeginAlternative:
|
||||
case HelloGenerationCommandEndChoice: {
|
||||
// should not be encountered directly at this level
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static NSMutableData *executeGenerationCode(id<EncryptionProvider> provider, NSData *domain) {
|
||||
NSString *code = @"S \"\\x16\\x03\\x01\\x02\\x00\\x01\\x00\\x01\\xfc\\x03\\x03\"\n"
|
||||
NSString *code = @"S \"\\x16\\x03\\x01\"\n"
|
||||
"[\n"
|
||||
"S \"\\x01\\x00\"\n"
|
||||
"[\n"
|
||||
"S \"\\x03\\x03\"\n"
|
||||
"Z 32\n"
|
||||
"S \"\\x20\"\n"
|
||||
"R 32\n"
|
||||
"S \"\\x00\\x2a\"\n"
|
||||
"G 0\n"
|
||||
"S \"\\x13\\x01\\x13\\x02\\x13\\x03\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\\x01\\x00\\x01\\x89\"\n"
|
||||
"<\n"
|
||||
"( S \"\\x00\\x1c\" G 0 S \"\\x13\\x02\\x13\\x01\\x13\\x03\\xc0\\x2c\\xc0\\x30\\xc0\\x2b\\xcc\\xa9\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\" )\n"
|
||||
"( S \"\\x00\\x2a\" G 0 S \"\\x13\\x02\\x13\\x03\\x13\\x01\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\" )\n"
|
||||
">\n"
|
||||
"S \"\\x01\\x00\"\n"
|
||||
"[\n"
|
||||
"G 2\n"
|
||||
"S \"\\x00\\x00\\x00\\x00\"\n"
|
||||
"[\n"
|
||||
|
|
@ -323,17 +502,28 @@ static NSMutableData *executeGenerationCode(id<EncryptionProvider> provider, NSD
|
|||
"]\n"
|
||||
"]\n"
|
||||
"]\n"
|
||||
"S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0c\\x00\\x0a\"\n"
|
||||
"S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0e\\x00\\x0c\"\n"
|
||||
"G 4\n"
|
||||
"S \"\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x00\\x2b\\x00\\x29\"\n"
|
||||
"S \"\\x11\\xec\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\"\n"
|
||||
"<\n"
|
||||
"( S \"\\x00\\x10\\x00\\x0b\\x00\\x09\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\" )\n"
|
||||
"( S \"\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\" )\n"
|
||||
">\n"
|
||||
"S \"\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x04\\xef\\x04\\xed\"\n"
|
||||
"G 4\n"
|
||||
"S \"\\x00\\x01\\x00\\x00\\x1d\\x00\\x20\"\n"
|
||||
"S \"\\x00\\x01\\x00\\x11\\xec\\x04\\xc0\"\n"
|
||||
"M\n"
|
||||
"K\n"
|
||||
"S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x0b\\x0a\"\n"
|
||||
"S \"\\x00\\x1d\\x00\\x20\"\n"
|
||||
"K\n"
|
||||
"S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x07\\x06\"\n"
|
||||
"G 6\n"
|
||||
"S \"\\x03\\x04\\x03\\x03\\x03\\x02\\x03\\x01\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n"
|
||||
"S \"\\x03\\x04\\x03\\x03\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n"
|
||||
"G 3\n"
|
||||
"S \"\\x00\\x01\\x00\"\n";
|
||||
"S \"\\x00\\x01\\x00\"\n"
|
||||
"]\n"
|
||||
"]\n"
|
||||
"]\n";
|
||||
|
||||
uint8_t grease[8];
|
||||
if (!MTGenerateGreaseValues(grease)) {
|
||||
|
|
@ -345,120 +535,8 @@ static NSMutableData *executeGenerationCode(id<EncryptionProvider> provider, NSD
|
|||
|
||||
HelloParseState state = { .position = 0 };
|
||||
|
||||
while (true) {
|
||||
HelloGenerationCommand command = parseCommand(code, &state);
|
||||
if (command == HelloGenerationCommandInvalid) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case HelloGenerationCommandString: {
|
||||
if (!parseSpace(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
NSData *data = parseHexStringArgument(code, &state);
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
[resultData appendData:data];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandZero: {
|
||||
if (!parseSpace(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
int zeroLength = 0;
|
||||
if (!parseIntArgument(code, &state, &zeroLength)) {
|
||||
return nil;
|
||||
}
|
||||
if (zeroLength < 0) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength];
|
||||
[resultData appendData:zeros];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandRandom: {
|
||||
if (!parseSpace(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
int randomLength = 0;
|
||||
if (!parseIntArgument(code, &state, &randomLength)) {
|
||||
return nil;
|
||||
}
|
||||
if (randomLength < 0) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength];
|
||||
if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) {
|
||||
return nil;
|
||||
}
|
||||
[resultData appendData:randomData];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandDomain: {
|
||||
[resultData appendData:domain];
|
||||
if (!parseEndlineOrEnd(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandGrease: {
|
||||
if (!parseSpace(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
int greaseIndex = 0;
|
||||
if (!parseIntArgument(code, &state, &greaseIndex)) {
|
||||
return nil;
|
||||
}
|
||||
if (greaseIndex < 0 || greaseIndex >= 8) {
|
||||
return nil;
|
||||
}
|
||||
uint8_t value = grease[greaseIndex];
|
||||
[resultData appendBytes:&value length:1];
|
||||
[resultData appendBytes:&value length:1];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandKey: {
|
||||
if (!parseEndlineOrEnd(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableData *key = [[NSMutableData alloc] initWithLength:32];
|
||||
generate_public_key((unsigned char *)key.mutableBytes, provider);
|
||||
[resultData appendData:key];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandPushLengthPosition: {
|
||||
if (!parseEndlineOrEnd(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
NSUInteger lengthPosition = resultData.length;
|
||||
uint8_t zeroBytes[2] = { 0, 0 };
|
||||
[resultData appendBytes:zeroBytes length:2];
|
||||
[lengthStack addObject:@(lengthPosition)];
|
||||
break;
|
||||
}
|
||||
case HelloGenerationCommandPopLengthPosition: {
|
||||
if (!parseEndlineOrEnd(code, &state)) {
|
||||
return nil;
|
||||
}
|
||||
if (lengthStack.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue];
|
||||
[lengthStack removeLastObject];
|
||||
if (resultData.length < position + 2) {
|
||||
return nil;
|
||||
}
|
||||
uint16_t blockLength = (uint16_t)(resultData.length - position - 2);
|
||||
((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff);
|
||||
((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
if (!executeGenerationCodeRecursive(code, &state, grease, domain, provider, resultData, lengthStack)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (lengthStack.count != 0) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import EntityKeyboard
|
|||
import ComponentDisplayAdapters
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import DCTMultiAnimationRendererImpl
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import GZip
|
||||
|
|
@ -504,8 +505,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.reactionsLocked = reactionsLocked
|
||||
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = MultiAnimationRendererImpl()
|
||||
(self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache
|
||||
self.animationRenderer = DCTMultiAnimationRendererImpl()
|
||||
(self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache
|
||||
|
||||
self.backgroundMaskNode = ASDisplayNode()
|
||||
var backgroundGlassParams: ReactionContextBackgroundNode.GlassParams?
|
||||
|
|
@ -3341,7 +3342,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||
if let currentItemNode = currentItemNode {
|
||||
itemNode = currentItemNode
|
||||
} else {
|
||||
let animationRenderer = MultiAnimationRendererImpl()
|
||||
let animationRenderer = DCTMultiAnimationRendererImpl()
|
||||
itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false)
|
||||
}
|
||||
self.itemNode = itemNode
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public final class SearchBarPlaceholderContentView: UIView {
|
|||
|
||||
let fieldStyle: SearchBarStyle
|
||||
let plainBackgroundView: UIImageView
|
||||
let glassBackgroundContainerView: GlassBackgroundContainerView?
|
||||
let glassBackgroundView: GlassBackgroundView?
|
||||
private var fillBackgroundColor: UIColor
|
||||
private var foregroundColor: UIColor
|
||||
|
|
@ -82,8 +83,10 @@ public final class SearchBarPlaceholderContentView: UIView {
|
|||
|
||||
switch fieldStyle {
|
||||
case .legacy, .modern:
|
||||
self.glassBackgroundContainerView = nil
|
||||
self.glassBackgroundView = nil
|
||||
case .inlineNavigation, .glass:
|
||||
self.glassBackgroundContainerView = GlassBackgroundContainerView()
|
||||
self.glassBackgroundView = GlassBackgroundView()
|
||||
}
|
||||
|
||||
|
|
@ -111,8 +114,9 @@ public final class SearchBarPlaceholderContentView: UIView {
|
|||
self.plainBackgroundView.addSubview(self.plainIconNode.view)
|
||||
self.plainBackgroundView.addSubview(self.plainLabelNode.view)
|
||||
|
||||
if let glassBackgroundView = self.glassBackgroundView {
|
||||
self.addSubview(glassBackgroundView)
|
||||
if let glassBackgroundContainerView = self.glassBackgroundContainerView, let glassBackgroundView = self.glassBackgroundView {
|
||||
self.addSubview(glassBackgroundContainerView)
|
||||
glassBackgroundContainerView.contentView.addSubview(glassBackgroundView)
|
||||
|
||||
glassBackgroundView.contentView.addSubview(self.iconNode.view)
|
||||
glassBackgroundView.contentView.addSubview(self.labelNode.view)
|
||||
|
|
@ -300,9 +304,14 @@ public final class SearchBarPlaceholderContentView: UIView {
|
|||
transition.updateFrame(view: self.plainBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.constrainedSize.width, height: height)))
|
||||
}
|
||||
|
||||
if let glassBackgroundView = self.glassBackgroundView {
|
||||
transition.updatePosition(layer: glassBackgroundView.layer, position: backgroundFrame.center)
|
||||
if let glassBackgroundContainerView = self.glassBackgroundContainerView, let glassBackgroundView = self.glassBackgroundView {
|
||||
|
||||
transition.updatePosition(layer: glassBackgroundContainerView.layer, position: backgroundFrame.center)
|
||||
transition.updateBounds(layer: glassBackgroundContainerView.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
|
||||
transition.updatePosition(layer: glassBackgroundView.layer, position: CGRect(origin: CGPoint(), size: backgroundFrame.size).center)
|
||||
transition.updateBounds(layer: glassBackgroundView.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
|
||||
var backgroundAlpha: CGFloat = 1.0
|
||||
if backgroundFrame.height < 16.0 {
|
||||
backgroundAlpha = max(0.0, min(1.0, backgroundFrame.height / 16.0))
|
||||
|
|
@ -310,9 +319,10 @@ public final class SearchBarPlaceholderContentView: UIView {
|
|||
if !params.isActive {
|
||||
backgroundAlpha = 0.0
|
||||
}
|
||||
ComponentTransition(transition).setAlpha(view: glassBackgroundView, alpha: backgroundAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: glassBackgroundContainerView, alpha: backgroundAlpha)
|
||||
let isDark = params.backgroundColor.hsb.b < 0.5
|
||||
if params.isActive {
|
||||
glassBackgroundContainerView.update(size: backgroundFrame.size, isDark: isDark, transition: ComponentTransition(transition))
|
||||
glassBackgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: isDark, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: true, transition: ComponentTransition(transition))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ private func shareLink(for server: ProxyServerSettings) -> String {
|
|||
switch server.connection {
|
||||
case let .mtp(secret):
|
||||
let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? ""
|
||||
link = "https://t.me/proxy?server=\(server.host)&port=\(server.port)"
|
||||
link = "tg://proxy?server=\(server.host)&port=\(server.port)"
|
||||
link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")"
|
||||
case let .socks5(username, password):
|
||||
link = "https://t.me/socks?server=\(server.host)&port=\(server.port)"
|
||||
|
|
@ -332,10 +332,10 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context:
|
|||
let signal = combineLatest(updatedPresentationData, statePromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
})
|
||||
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: {
|
||||
let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: {
|
||||
if let proxyServerSettings = proxyServerSettings(with: state) {
|
||||
let _ = (updateProxySettingsInteractively(accountManager: accountManager, { settings in
|
||||
var settings = settings
|
||||
|
|
|
|||
|
|
@ -283,6 +283,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl",
|
||||
"//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl",
|
||||
"//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent",
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ swift_library(
|
|||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/CryptoUtils:CryptoUtils",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
"//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -42,6 +42,8 @@ swift_library(
|
|||
"//submodules/AttachmentTextInputPanelNode",
|
||||
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl",
|
||||
"//submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl:SubcodecMultiAnimationRendererImpl",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
23
submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD
Normal file
23
submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "DCTAnimationCacheImpl",
|
||||
module_name = "DCTAnimationCacheImpl",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/CryptoUtils:CryptoUtils",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT",
|
||||
"//third-party/subcodec:SubcodecObjC",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AnimationCache
|
||||
import ImageDCT
|
||||
import Accelerate
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "DCTMultiAnimationRendererImpl",
|
||||
module_name = "DCTMultiAnimationRendererImpl",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import AnimationCache
|
|||
import LottieAnimationCache
|
||||
import VideoAnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import DCTMultiAnimationRendererImpl
|
||||
import ShimmerEffect
|
||||
import TextFormat
|
||||
import TelegramUIPreferences
|
||||
|
|
@ -817,7 +818,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||
let isThumbnailCancelled = Atomic<Bool>(value: false)
|
||||
self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in
|
||||
if !result {
|
||||
MultiAnimationRendererImpl.firstFrameQueue.async {
|
||||
DCTMultiAnimationRendererImpl.firstFrameQueue.async {
|
||||
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Postbox
|
|||
import SwiftSignalKit
|
||||
import MultiAnimationRenderer
|
||||
import AnimationCache
|
||||
import DCTMultiAnimationRendererImpl
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
import GenerateStickerPlaceholderImage
|
||||
|
|
@ -277,7 +278,7 @@ public final class InlineFileIconLayer: MultiAnimationRenderTarget {
|
|||
size: arguments.pixelSize,
|
||||
fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in
|
||||
if !result {
|
||||
MultiAnimationRendererImpl.firstFrameQueue.async {
|
||||
DCTMultiAnimationRendererImpl.firstFrameQueue.async {
|
||||
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,4 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
load(
|
||||
"@build_bazel_rules_apple//apple:resources.bzl",
|
||||
"apple_resource_bundle",
|
||||
"apple_resource_group",
|
||||
)
|
||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
"plist_fragment",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "MultiAnimationRendererMetalResources",
|
||||
srcs = glob([
|
||||
"Resources/**/*.metal",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "WallpaperBackgroundNodeBundleInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.telegram.MultiAnimationRenderer</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>MultiAnimationRenderer</string>
|
||||
"""
|
||||
)
|
||||
|
||||
apple_resource_bundle(
|
||||
name = "MultiAnimationRendererBundle",
|
||||
infoplists = [
|
||||
":WallpaperBackgroundNodeBundleInfoPlist",
|
||||
],
|
||||
resources = [
|
||||
":MultiAnimationRendererMetalResources",
|
||||
],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "MultiAnimationRenderer",
|
||||
|
|
@ -46,9 +6,6 @@ swift_library(
|
|||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
data = [
|
||||
":MultiAnimationRendererBundle",
|
||||
],
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
typedef struct {
|
||||
packed_float2 position;
|
||||
packed_float2 texCoord;
|
||||
} Vertex;
|
||||
|
||||
typedef struct {
|
||||
float4 position[[position]];
|
||||
float2 texCoord;
|
||||
} Varyings;
|
||||
|
||||
vertex Varyings multiAnimationVertex(
|
||||
unsigned int vid[[vertex_id]],
|
||||
constant Vertex *verticies[[buffer(0)]],
|
||||
constant uint2 &resolution[[buffer(1)]],
|
||||
constant uint2 &slotSize[[buffer(2)]],
|
||||
constant uint2 &slotPosition[[buffer(3)]]
|
||||
) {
|
||||
Varyings out;
|
||||
constant Vertex &v = verticies[vid];
|
||||
|
||||
out.position = float4(float2(v.position), 0.0, 1.0);
|
||||
out.texCoord = v.texCoord;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment half4 multiAnimationFragment(
|
||||
Varyings in[[stage_in]],
|
||||
texture2d<float, access::sample> textureY[[texture(0)]],
|
||||
texture2d<float, access::sample> textureU[[texture(1)]],
|
||||
texture2d<float, access::sample> textureV[[texture(2)]],
|
||||
texture2d<float, access::sample> textureA[[texture(3)]]
|
||||
) {
|
||||
constexpr sampler s(address::clamp_to_edge, filter::linear);
|
||||
|
||||
half y = textureY.sample(s, in.texCoord).r;
|
||||
half u = textureU.sample(s, in.texCoord).r - 0.5;
|
||||
half v = textureV.sample(s, in.texCoord).r - 0.5;
|
||||
half a = textureA.sample(s, in.texCoord).r;
|
||||
|
||||
half4 out = half4(1.5748 * v + y, -0.1873 * v + y, 1.8556 * u + y, a);
|
||||
return half4(out.b, out.g, out.r, out.a);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -36,6 +36,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/TelegramUIDeclareEncodables",
|
||||
"//submodules/TelegramUI/Components/AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl",
|
||||
"//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl",
|
||||
"//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods",
|
||||
"//submodules/TelegramUI/Components/PeerSelectionController",
|
||||
"//submodules/TelegramUI/Components/ContextMenuScreen",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import ManagedFile
|
|||
import TelegramUIDeclareEncodables
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import DCTAnimationCacheImpl
|
||||
import DCTMultiAnimationRendererImpl
|
||||
import TelegramUIDeclareEncodables
|
||||
import TelegramAccountAuxiliaryMethods
|
||||
import PeerSelectionController
|
||||
|
|
@ -105,14 +107,14 @@ private final class ShareControllerAccountContextExtension: ShareControllerAccou
|
|||
self.stateManager = stateManager
|
||||
self.engineData = TelegramEngine.EngineData(accountPeerId: stateManager.accountPeerId, postbox: stateManager.postbox)
|
||||
let cacheStorageBox = stateManager.postbox.mediaBox.cacheStorageBox
|
||||
self.animationCache = AnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
self.animationCache = DCTAnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
return TempBox.shared.tempFile(fileName: "file").path
|
||||
}, updateStorageStats: { path, size in
|
||||
if let pathData = path.data(using: .utf8) {
|
||||
cacheStorageBox.update(id: pathData, size: size)
|
||||
}
|
||||
})
|
||||
self.animationRenderer = MultiAnimationRendererImpl()
|
||||
self.animationRenderer = DCTMultiAnimationRendererImpl()
|
||||
self.contentSettings = contentSettings
|
||||
self.appConfiguration = appConfiguration
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1689,14 +1689,15 @@ public final class StoryPeerListComponent: Component {
|
|||
NSAttributedString.Key.font: Font.semibold(17.0),
|
||||
NSAttributedString.Key.foregroundColor: component.theme.rootController.navigationBar.primaryTextColor
|
||||
])
|
||||
var boundingRect = attributedText.boundingRect(with: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 30.0), height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
boundingRect.size.width = ceil(boundingRect.size.width)
|
||||
boundingRect.size.height = ceil(boundingRect.size.height)
|
||||
|
||||
let cachedLayout = TextNode.calculateLayout(attributedString: attributedText, minimumNumberOfLines: 1, maximumNumberOfLines: 1, truncationType: .end, backgroundColor: nil, constrainedSize: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 46.0), height: 100.0), alignment: .left, verticalAlignment: .middle, lineSpacingFactor: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textShadowBlur: nil, textStroke: nil, displaySpoilers: false, displayEmbeddedItemsUnderSpoilers: false, customTruncationToken: nil)
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: boundingRect.size))
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: cachedLayout.size))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
attributedText.draw(at: CGPoint())
|
||||
|
||||
TextNode.draw(CGRect(origin: CGPoint(), size: cachedLayout.size), withParameters: TextNode.DrawingParameters(cachedLayout: cachedLayout, renderContentTypes: .all), isCancelled: { return false }, isRasterizing: false)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
self.titleView.image = image
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SubcodecAnimationCacheImpl",
|
||||
module_name = "SubcodecAnimationCacheImpl",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/CryptoUtils:CryptoUtils",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//third-party/subcodec:SubcodecObjC",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,800 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import CryptoUtils
|
||||
import ManagedFile
|
||||
import AnimationCache
|
||||
import SubcodecObjC
|
||||
|
||||
public struct MbsMetadata {
|
||||
public let frameCount: Int
|
||||
public let frameDurations: [Double]
|
||||
}
|
||||
|
||||
private func md5Hash(_ string: String) -> String {
|
||||
let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in
|
||||
return CryptoMD5(bytes.baseAddress!, Int32(bytes.count))
|
||||
}
|
||||
return hashData.withUnsafeBytes { bytes -> String in
|
||||
let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15])
|
||||
}
|
||||
}
|
||||
|
||||
private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) {
|
||||
assert(hashString.count == 32)
|
||||
let directory = String(hashString[hashString.startIndex ..< hashString.index(hashString.startIndex, offsetBy: 2)])
|
||||
return (directory, "\(hashString)_\(width)x\(height)")
|
||||
}
|
||||
|
||||
private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
|
||||
if multiple == 0 {
|
||||
return numToRound
|
||||
}
|
||||
let remainder = numToRound % multiple
|
||||
if remainder == 0 {
|
||||
return numToRound
|
||||
}
|
||||
return numToRound + multiple - remainder
|
||||
}
|
||||
|
||||
private func convertARGBToYUVA420(
|
||||
argb: UnsafePointer<UInt8>,
|
||||
width: Int,
|
||||
height: Int,
|
||||
bytesPerRow: Int
|
||||
) -> (y: Data, cb: Data, cr: Data, alpha: Data, yStride: Int, cbStride: Int, crStride: Int, alphaStride: Int) {
|
||||
let chromaWidth = width / 2
|
||||
let chromaHeight = height / 2
|
||||
|
||||
var yData = Data(count: width * height)
|
||||
var cbData = Data(count: chromaWidth * chromaHeight)
|
||||
var crData = Data(count: chromaWidth * chromaHeight)
|
||||
var alphaData = Data(count: width * height)
|
||||
|
||||
yData.withUnsafeMutableBytes { yBuf in
|
||||
cbData.withUnsafeMutableBytes { cbBuf in
|
||||
crData.withUnsafeMutableBytes { crBuf in
|
||||
alphaData.withUnsafeMutableBytes { aBuf in
|
||||
let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let aPtr = aBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
for row in 0 ..< height {
|
||||
let srcRow = argb.advanced(by: row * bytesPerRow)
|
||||
for col in 0 ..< width {
|
||||
let px = srcRow.advanced(by: col * 4)
|
||||
// BGRA layout (CoreGraphics premultiplied)
|
||||
let b = Int(px[0])
|
||||
let g = Int(px[1])
|
||||
let r = Int(px[2])
|
||||
let a = Int(px[3])
|
||||
|
||||
// Un-premultiply
|
||||
let rr: Int
|
||||
let gg: Int
|
||||
let bb: Int
|
||||
if a > 0 {
|
||||
rr = min(255, r * 255 / a)
|
||||
gg = min(255, g * 255 / a)
|
||||
bb = min(255, b * 255 / a)
|
||||
} else {
|
||||
rr = 0
|
||||
gg = 0
|
||||
bb = 0
|
||||
}
|
||||
|
||||
// BT.709
|
||||
let y = 16 + (65 * rr + 129 * gg + 25 * bb + 128) / 256
|
||||
yPtr[row * width + col] = UInt8(clamping: y)
|
||||
aPtr[row * width + col] = UInt8(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Chroma at half resolution (average 2x2 blocks)
|
||||
for row in 0 ..< chromaHeight {
|
||||
for col in 0 ..< chromaWidth {
|
||||
var sumR = 0
|
||||
var sumG = 0
|
||||
var sumB = 0
|
||||
for dy in 0 ..< 2 {
|
||||
for dx in 0 ..< 2 {
|
||||
let srcRow = argb.advanced(by: (row * 2 + dy) * bytesPerRow)
|
||||
let px = srcRow.advanced(by: (col * 2 + dx) * 4)
|
||||
let b = Int(px[0])
|
||||
let g = Int(px[1])
|
||||
let r = Int(px[2])
|
||||
let a = Int(px[3])
|
||||
if a > 0 {
|
||||
sumR += min(255, r * 255 / a)
|
||||
sumG += min(255, g * 255 / a)
|
||||
sumB += min(255, b * 255 / a)
|
||||
}
|
||||
}
|
||||
}
|
||||
let avgR = sumR / 4
|
||||
let avgG = sumG / 4
|
||||
let avgB = sumB / 4
|
||||
|
||||
let cb = 128 + (-38 * avgR - 74 * avgG + 112 * avgB + 128) / 256
|
||||
let cr = 128 + (112 * avgR - 94 * avgG - 18 * avgB + 128) / 256
|
||||
cbPtr[row * chromaWidth + col] = UInt8(clamping: cb)
|
||||
crPtr[row * chromaWidth + col] = UInt8(clamping: cr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (yData, cbData, crData, alphaData, width, chromaWidth, chromaWidth, width)
|
||||
}
|
||||
|
||||
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||
struct CompressedResult {
|
||||
var mbsPath: String
|
||||
var metaPath: String
|
||||
}
|
||||
|
||||
var queue: Queue {
|
||||
return self.innerQueue
|
||||
}
|
||||
let innerQueue: Queue
|
||||
var isCancelled: Bool = false
|
||||
|
||||
private let mbsOutputPath: String
|
||||
private let metaOutputPath: String
|
||||
private let completion: (CompressedResult?) -> Void
|
||||
|
||||
private var spriteExtractor: SCSprite?
|
||||
private var frameDurations: [Double] = []
|
||||
private var isFailed: Bool = false
|
||||
private var isFinished: Bool = false
|
||||
private var spriteWidth: Int = 0
|
||||
private var spriteHeight: Int = 0
|
||||
|
||||
private let lock = Lock()
|
||||
|
||||
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
|
||||
self.innerQueue = queue
|
||||
self.mbsOutputPath = allocateTempFile()
|
||||
self.metaOutputPath = allocateTempFile()
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) {
|
||||
self.lock.locked {
|
||||
if self.isFailed || self.isFinished {
|
||||
return
|
||||
}
|
||||
|
||||
let width = roundUp(proposedWidth, multiple: 16)
|
||||
let height = roundUp(proposedHeight, multiple: 16)
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
// Create extractor on first frame
|
||||
if self.spriteExtractor == nil {
|
||||
self.spriteWidth = width
|
||||
self.spriteHeight = height
|
||||
let spriteSize = max(width, height)
|
||||
do {
|
||||
self.spriteExtractor = try SCSprite.extractor(withSpriteSize: Int32(spriteSize), qp: 26, outputPath: self.mbsOutputPath)
|
||||
} catch {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard self.spriteWidth == width && self.spriteHeight == height else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
// Allocate ARGB surface
|
||||
let bytesPerRow = width * 4
|
||||
let bufferSize = height * bytesPerRow
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
defer { buffer.deallocate() }
|
||||
memset(buffer, 0, bufferSize)
|
||||
|
||||
guard let duration = drawingBlock(AnimationCacheItemDrawingSurface(
|
||||
argb: buffer,
|
||||
width: width,
|
||||
height: height,
|
||||
bytesPerRow: bytesPerRow,
|
||||
length: bufferSize
|
||||
)) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert ARGB → YUV planes
|
||||
let planes = convertARGBToYUVA420(
|
||||
argb: buffer,
|
||||
width: width,
|
||||
height: height,
|
||||
bytesPerRow: bytesPerRow
|
||||
)
|
||||
|
||||
// Feed to extractor
|
||||
do {
|
||||
try self.spriteExtractor?.addFrameY(
|
||||
planes.y, yStride: Int32(planes.yStride),
|
||||
cb: planes.cb, cbStride: Int32(planes.cbStride),
|
||||
cr: planes.cr, crStride: Int32(planes.crStride),
|
||||
alpha: planes.alpha, alphaStride: Int32(planes.alphaStride)
|
||||
)
|
||||
} catch {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
self.frameDurations.append(duration)
|
||||
}
|
||||
}
|
||||
|
||||
func finish() {
|
||||
var result: CompressedResult?
|
||||
|
||||
self.lock.locked {
|
||||
if self.isFinished {
|
||||
return
|
||||
}
|
||||
self.isFinished = true
|
||||
|
||||
if self.isFailed || self.spriteExtractor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try self.spriteExtractor?.finalizeExtraction()
|
||||
} catch {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
// Write metadata file: frame count + durations
|
||||
var metaData = Data()
|
||||
var frameCount = UInt32(self.frameDurations.count)
|
||||
metaData.append(Data(bytes: &frameCount, count: 4))
|
||||
for duration in self.frameDurations {
|
||||
var d = Float32(duration)
|
||||
metaData.append(Data(bytes: &d, count: 4))
|
||||
}
|
||||
do {
|
||||
try metaData.write(to: URL(fileURLWithPath: self.metaOutputPath))
|
||||
} catch {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
result = CompressedResult(mbsPath: self.mbsOutputPath, metaPath: self.metaOutputPath)
|
||||
}
|
||||
|
||||
if !self.isFailed {
|
||||
self.completion(result)
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: self.mbsOutputPath)
|
||||
let _ = try? FileManager.default.removeItem(atPath: self.metaOutputPath)
|
||||
self.completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeSingleFrameFromMbs(mbsPath: String, metadata: MbsMetadata) -> AnimationCacheItemFrame? {
|
||||
guard let mbsData = try? Data(contentsOf: URL(fileURLWithPath: mbsPath), options: .mappedIfSafe) else {
|
||||
return nil
|
||||
}
|
||||
guard mbsData.count >= 14 else {
|
||||
return nil
|
||||
}
|
||||
guard mbsData[0] == 0x4D, mbsData[1] == 0x42, mbsData[2] == 0x53, mbsData[3] == 0x36 else {
|
||||
return nil
|
||||
}
|
||||
let _ = Int(mbsData[4]) | (Int(mbsData[5]) << 8)
|
||||
let heightMbs = Int(mbsData[6]) | (Int(mbsData[7]) << 8)
|
||||
let qp = Int(mbsData[10])
|
||||
|
||||
let spriteContentSize = heightMbs * 16 - 32
|
||||
guard spriteContentSize > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nalData: Data?
|
||||
let sinkBlock: (Data) -> Void = { data in
|
||||
if nalData == nil {
|
||||
nalData = data
|
||||
} else {
|
||||
nalData!.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
guard let surface = try? SCMuxSurface.create(
|
||||
withSpriteWidth: Int32(spriteContentSize),
|
||||
spriteHeight: Int32(spriteContentSize),
|
||||
maxSlots: 1,
|
||||
qp: Int32(qp),
|
||||
sink: sinkBlock
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let region = try? surface.addSprite(atPath: mbsPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
nalData = nil
|
||||
guard let _ = try? surface.advanceFrame(sink: sinkBlock) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let streamData = nalData else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let decoder = try? SCVideoToolboxDecoder.createDecoder() else {
|
||||
return nil
|
||||
}
|
||||
guard let frames = try? decoder.decodeStream(streamData) else {
|
||||
return nil
|
||||
}
|
||||
guard let decodedFrame = frames.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return extractSpriteFrame(
|
||||
decodedFrame: decodedFrame,
|
||||
region: region,
|
||||
spriteContentWidth: spriteContentSize,
|
||||
spriteContentHeight: spriteContentSize
|
||||
)
|
||||
}
|
||||
|
||||
private func extractSpriteFrame(
|
||||
decodedFrame: SCDecodedFrame,
|
||||
region: SCSpriteRegion,
|
||||
spriteContentWidth: Int,
|
||||
spriteContentHeight: Int
|
||||
) -> AnimationCacheItemFrame? {
|
||||
let colorRect = region.colorRect
|
||||
let alphaRect = region.alphaRect
|
||||
|
||||
let frameWidth = Int(colorRect.width)
|
||||
let frameHeight = Int(colorRect.height)
|
||||
|
||||
guard frameWidth > 0 && frameHeight > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let decodedWidth = Int(decodedFrame.width)
|
||||
|
||||
let bytesPerRow = frameWidth * 4
|
||||
var argbData = Data(count: frameHeight * bytesPerRow)
|
||||
|
||||
decodedFrame.y.withUnsafeBytes { yBuf in
|
||||
decodedFrame.cb.withUnsafeBytes { cbBuf in
|
||||
decodedFrame.cr.withUnsafeBytes { crBuf in
|
||||
argbData.withUnsafeMutableBytes { outBuf in
|
||||
let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let outPtr = outBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
let colorX = Int(colorRect.origin.x)
|
||||
let colorY = Int(colorRect.origin.y)
|
||||
let alphaX = Int(alphaRect.origin.x)
|
||||
let alphaY = Int(alphaRect.origin.y)
|
||||
|
||||
let chromaWidth = decodedWidth / 2
|
||||
|
||||
for row in 0 ..< frameHeight {
|
||||
for col in 0 ..< frameWidth {
|
||||
let srcY = yPtr[(colorY + row) * decodedWidth + (colorX + col)]
|
||||
let srcCb = cbPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)]
|
||||
let srcCr = crPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)]
|
||||
let srcA = yPtr[(alphaY + row) * decodedWidth + (alphaX + col)]
|
||||
|
||||
let yVal = Int(srcY) - 16
|
||||
let cbVal = Int(srcCb) - 128
|
||||
let crVal = Int(srcCr) - 128
|
||||
|
||||
var r = (298 * yVal + 459 * crVal + 128) >> 8
|
||||
var g = (298 * yVal - 55 * cbVal - 136 * crVal + 128) >> 8
|
||||
var b = (298 * yVal + 541 * cbVal + 128) >> 8
|
||||
|
||||
r = max(0, min(255, r))
|
||||
g = max(0, min(255, g))
|
||||
b = max(0, min(255, b))
|
||||
|
||||
let a = Int(srcA)
|
||||
|
||||
let pr = (r * a + 127) / 255
|
||||
let pg = (g * a + 127) / 255
|
||||
let pb = (b * a + 127) / 255
|
||||
|
||||
let outOffset = (row * frameWidth + col) * 4
|
||||
outPtr[outOffset + 0] = UInt8(pb)
|
||||
outPtr[outOffset + 1] = UInt8(pg)
|
||||
outPtr[outOffset + 2] = UInt8(pr)
|
||||
outPtr[outOffset + 3] = UInt8(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AnimationCacheItemFrame(
|
||||
format: .rgba(data: argbData, width: frameWidth, height: frameHeight, bytesPerRow: bytesPerRow),
|
||||
duration: 1.0 / 30.0
|
||||
)
|
||||
}
|
||||
|
||||
private func loadMbsMetadata(metaPath: String) -> MbsMetadata? {
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: metaPath), options: .mappedIfSafe) else {
|
||||
return nil
|
||||
}
|
||||
guard data.count >= 4 else {
|
||||
return nil
|
||||
}
|
||||
var frameCount: UInt32 = 0
|
||||
withUnsafeMutableBytes(of: &frameCount) { buf in
|
||||
data.copyBytes(to: buf.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 0 ..< 4)
|
||||
}
|
||||
let expectedSize = 4 + Int(frameCount) * 4
|
||||
guard data.count >= expectedSize else {
|
||||
return nil
|
||||
}
|
||||
var durations: [Double] = []
|
||||
for i in 0 ..< Int(frameCount) {
|
||||
var d: Float32 = 0
|
||||
let offset = 4 + i * 4
|
||||
withUnsafeMutableBytes(of: &d) { buf in
|
||||
data.copyBytes(to: buf.baseAddress!.assumingMemoryBound(to: UInt8.self), from: offset ..< offset + 4)
|
||||
}
|
||||
durations.append(Double(d))
|
||||
}
|
||||
return MbsMetadata(frameCount: Int(frameCount), frameDurations: durations)
|
||||
}
|
||||
|
||||
public final class SubcodecAnimationCacheImpl: AnimationCache {
|
||||
private final class Impl {
|
||||
private struct ItemKey: Hashable {
|
||||
var id: String
|
||||
var width: Int
|
||||
var height: Int
|
||||
}
|
||||
|
||||
private final class ItemContext {
|
||||
let subscribers = Bag<(AnimationCacheItemResult) -> Void>()
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let basePath: String
|
||||
private let allocateTempFile: () -> String
|
||||
private let updateStorageStats: (String, Int64) -> Void
|
||||
|
||||
private let fetchQueues: [Queue]
|
||||
private var nextFetchQueueIndex: Int = 0
|
||||
|
||||
private var itemContexts: [ItemKey: ItemContext] = [:]
|
||||
|
||||
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) {
|
||||
self.queue = queue
|
||||
|
||||
let fetchQueueCount: Int
|
||||
if ProcessInfo.processInfo.processorCount > 2 {
|
||||
fetchQueueCount = 3
|
||||
} else {
|
||||
fetchQueueCount = 2
|
||||
}
|
||||
self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "SubcodecAnimationCacheImpl-Fetch\(i)", qos: .default) }
|
||||
self.basePath = basePath
|
||||
self.allocateTempFile = allocateTempFile
|
||||
self.updateStorageStats = updateStorageStats
|
||||
}
|
||||
|
||||
func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
|
||||
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||
let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs"
|
||||
let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta"
|
||||
|
||||
if FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) {
|
||||
if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) {
|
||||
let item = AnimationCacheItem(numFrames: metadata.frameCount, advanceImpl: { _, _ in
|
||||
return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false)
|
||||
}, resetImpl: {})
|
||||
updateResult(AnimationCacheItemResult(item: item, isFinal: true))
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height))
|
||||
|
||||
let itemContext: ItemContext
|
||||
var beginFetch = false
|
||||
if let current = self.itemContexts[key] {
|
||||
itemContext = current
|
||||
} else {
|
||||
itemContext = ItemContext()
|
||||
self.itemContexts[key] = itemContext
|
||||
beginFetch = true
|
||||
}
|
||||
|
||||
let queue = self.queue
|
||||
let index = itemContext.subscribers.add(updateResult)
|
||||
|
||||
updateResult(AnimationCacheItemResult(item: nil, isFinal: false))
|
||||
|
||||
if beginFetch {
|
||||
let fetchQueueIndex = self.nextFetchQueueIndex
|
||||
self.nextFetchQueueIndex += 1
|
||||
let allocateTempFile = self.allocateTempFile
|
||||
let updateStorageStats = self.updateStorageStats
|
||||
|
||||
guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: allocateTempFile, completion: { [weak self, weak itemContext] result in
|
||||
queue.async {
|
||||
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.itemContexts.removeValue(forKey: key)
|
||||
|
||||
guard let result = result else {
|
||||
for f in itemContext.subscribers.copyItems() {
|
||||
f(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
|
||||
return
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: mbsPath)
|
||||
let _ = try? FileManager.default.removeItem(atPath: metaPath)
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: result.mbsPath, toPath: mbsPath) else {
|
||||
return
|
||||
}
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: result.metaPath, toPath: metaPath) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let size = SubcodecAnimationCacheImpl.fileSize(mbsPath) {
|
||||
updateStorageStats(mbsPath, size)
|
||||
}
|
||||
|
||||
guard let metadata = loadMbsMetadata(metaPath: metaPath) else {
|
||||
return
|
||||
}
|
||||
|
||||
for f in itemContext.subscribers.copyItems() {
|
||||
if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) {
|
||||
let item = AnimationCacheItem(numFrames: metadata.frameCount, advanceImpl: { _, _ in
|
||||
return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false)
|
||||
}, resetImpl: {})
|
||||
f(AnimationCacheItemResult(item: item, isFinal: true))
|
||||
} else {
|
||||
f(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}) else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let fetchDisposable = MetaDisposable()
|
||||
fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false)))
|
||||
|
||||
itemContext.disposable.set(ActionDisposable { [weak writer] in
|
||||
if let writer = writer {
|
||||
writer.isCancelled = true
|
||||
}
|
||||
fetchDisposable.dispose()
|
||||
})
|
||||
}
|
||||
|
||||
return ActionDisposable { [weak self, weak itemContext] in
|
||||
queue.async {
|
||||
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else {
|
||||
return
|
||||
}
|
||||
itemContext.subscribers.remove(index)
|
||||
if itemContext.subscribers.isEmpty {
|
||||
itemContext.disposable.dispose()
|
||||
strongSelf.itemContexts.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func fileSize(_ path: String) -> Int64? {
|
||||
var value = stat()
|
||||
if stat(path, &value) == 0 {
|
||||
return value.st_size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let basePath: String
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
private let allocateTempFile: () -> String
|
||||
private let updateStorageStats: (String, Int64) -> Void
|
||||
|
||||
public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) {
|
||||
let queue = Queue()
|
||||
self.queue = queue
|
||||
self.basePath = basePath
|
||||
self.allocateTempFile = allocateTempFile
|
||||
self.updateStorageStats = updateStorageStats
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats)
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - AnimationCache protocol
|
||||
|
||||
public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal<AnimationCacheItemResult, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in
|
||||
subscriber.putNext(result)
|
||||
if result.isFinal {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
|> runOn(self.queue)
|
||||
}
|
||||
|
||||
public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
|
||||
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||
let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs"
|
||||
let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta"
|
||||
|
||||
guard FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) else {
|
||||
return nil
|
||||
}
|
||||
guard let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) else {
|
||||
return nil
|
||||
}
|
||||
return AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in
|
||||
return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false)
|
||||
}, resetImpl: {})
|
||||
}
|
||||
|
||||
public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
|
||||
let disposable = MetaDisposable()
|
||||
let basePath = self.basePath
|
||||
let allocateTempFile = self.allocateTempFile
|
||||
let updateStorageStats = self.updateStorageStats
|
||||
|
||||
queue.async {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
|
||||
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
|
||||
let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs"
|
||||
let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta"
|
||||
|
||||
if FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) {
|
||||
if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) {
|
||||
let item = AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in
|
||||
return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false)
|
||||
}, resetImpl: {})
|
||||
completion(AnimationCacheItemResult(item: item, isFinal: true))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let fetch = fetch {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: false))
|
||||
guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in
|
||||
queue.async {
|
||||
guard let result = result else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: mbsPath)
|
||||
let _ = try? FileManager.default.removeItem(atPath: metaPath)
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: result.mbsPath, toPath: mbsPath) else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: result.metaPath, toPath: metaPath) else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
if let size = SubcodecAnimationCacheImpl.fileSize(mbsPath) {
|
||||
updateStorageStats(mbsPath, size)
|
||||
}
|
||||
guard let metadata = loadMbsMetadata(metaPath: metaPath) else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
guard let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
let item = AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in
|
||||
return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false)
|
||||
}, resetImpl: {})
|
||||
completion(AnimationCacheItemResult(item: item, isFinal: true))
|
||||
}
|
||||
}) else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
return
|
||||
}
|
||||
let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true))
|
||||
disposable.set(fetchDisposable)
|
||||
} else {
|
||||
completion(AnimationCacheItemResult(item: nil, isFinal: true))
|
||||
}
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
|
||||
// MARK: - Subcodec-specific API (used by renderer)
|
||||
|
||||
public func getMbsPath(sourceId: String, size: CGSize) -> String {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
|
||||
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||
return "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs"
|
||||
}
|
||||
|
||||
public func getMetaPath(sourceId: String, size: CGSize) -> String {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
|
||||
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||
return "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta"
|
||||
}
|
||||
|
||||
public func loadMetadata(sourceId: String, size: CGSize) -> MbsMetadata? {
|
||||
let metaPath = getMetaPath(sourceId: sourceId, size: size)
|
||||
return loadMbsMetadata(metaPath: metaPath)
|
||||
}
|
||||
|
||||
public func fetchIfNeeded(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, completion: @escaping (String?) -> Void) -> Disposable {
|
||||
let mbsPath = getMbsPath(sourceId: sourceId, size: size)
|
||||
if FileManager.default.fileExists(atPath: mbsPath) {
|
||||
completion(mbsPath)
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
if result.isFinal {
|
||||
let path = strongSelf.getMbsPath(sourceId: sourceId, size: size)
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
completion(path)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SubcodecMultiAnimationRendererImpl",
|
||||
module_name = "SubcodecMultiAnimationRendererImpl",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl",
|
||||
"//third-party/subcodec:SubcodecObjC",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,653 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import SubcodecObjC
|
||||
import SubcodecAnimationCacheImpl
|
||||
import Accelerate
|
||||
|
||||
private let maxSlotsLimit: Int = 882
|
||||
|
||||
private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
|
||||
if multiple == 0 { return numToRound }
|
||||
let remainder = numToRound % multiple
|
||||
if remainder == 0 { return numToRound }
|
||||
return numToRound + multiple - remainder
|
||||
}
|
||||
|
||||
private final class SpriteContext {
|
||||
let mbsPath: String
|
||||
let region: SCSpriteRegion
|
||||
let targets = Bag<Weak<MultiAnimationRenderTarget>>()
|
||||
let metadata: MbsMetadata
|
||||
|
||||
var currentFrameIndex: Int = 0
|
||||
var remainingDuration: Double = 0.0
|
||||
|
||||
var isPlaying: Bool {
|
||||
for target in self.targets.copyItems() {
|
||||
if let target = target.value, target.shouldBeAnimating {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
init(mbsPath: String, region: SCSpriteRegion, metadata: MbsMetadata) {
|
||||
self.mbsPath = mbsPath
|
||||
self.region = region
|
||||
self.metadata = metadata
|
||||
if !metadata.frameDurations.isEmpty {
|
||||
self.remainingDuration = metadata.frameDurations[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class SurfaceGroup {
|
||||
let spriteWidth: Int
|
||||
let spriteHeight: Int
|
||||
|
||||
private(set) var surface: SCMuxSurface?
|
||||
private(set) var decoder: SCVideoToolboxDecoder?
|
||||
private(set) var spriteContexts: [Int: SpriteContext] = [:]
|
||||
private var currentMaxSlots: Int = 64
|
||||
private var accumulatedNalData = Data()
|
||||
private var lastDecodedFrame: SCDecodedFrame?
|
||||
|
||||
init(spriteWidth: Int, spriteHeight: Int) {
|
||||
self.spriteWidth = spriteWidth
|
||||
self.spriteHeight = spriteHeight
|
||||
}
|
||||
|
||||
func ensureInitialized() -> Bool {
|
||||
if self.surface != nil {
|
||||
return true
|
||||
}
|
||||
let sink: (Data) -> Void = { [weak self] data in
|
||||
self?.accumulatedNalData.append(data)
|
||||
}
|
||||
guard let surface = try? SCMuxSurface.create(
|
||||
withSpriteWidth: Int32(self.spriteWidth),
|
||||
spriteHeight: Int32(self.spriteHeight),
|
||||
maxSlots: Int32(self.currentMaxSlots),
|
||||
qp: 26,
|
||||
sink: sink
|
||||
) else {
|
||||
return false
|
||||
}
|
||||
self.surface = surface
|
||||
|
||||
guard let decoder = try? SCVideoToolboxDecoder.createDecoder() else {
|
||||
return false
|
||||
}
|
||||
self.decoder = decoder
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func addSprite(mbsPath: String, metadata: MbsMetadata) -> SpriteContext? {
|
||||
guard self.ensureInitialized(), let surface = self.surface else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let region = try? surface.addSprite(atPath: mbsPath) else {
|
||||
if !self.tryGrow() {
|
||||
return nil
|
||||
}
|
||||
guard let region = try? surface.addSprite(atPath: mbsPath) else {
|
||||
return nil
|
||||
}
|
||||
let context = SpriteContext(mbsPath: mbsPath, region: region, metadata: metadata)
|
||||
self.spriteContexts[Int(region.slot)] = context
|
||||
return context
|
||||
}
|
||||
|
||||
let context = SpriteContext(mbsPath: mbsPath, region: region, metadata: metadata)
|
||||
self.spriteContexts[Int(region.slot)] = context
|
||||
return context
|
||||
}
|
||||
|
||||
func removeSprite(slot: Int) {
|
||||
self.surface?.removeSprite(atSlot: Int32(slot))
|
||||
self.spriteContexts.removeValue(forKey: slot)
|
||||
}
|
||||
|
||||
private func tryGrow() -> Bool {
|
||||
guard let surface = self.surface, let decoder = self.decoder else {
|
||||
return false
|
||||
}
|
||||
|
||||
let newMaxSlots: Int
|
||||
if self.currentMaxSlots * 2 <= maxSlotsLimit {
|
||||
newMaxSlots = self.currentMaxSlots * 2
|
||||
} else if self.currentMaxSlots < maxSlotsLimit {
|
||||
newMaxSlots = maxSlotsLimit
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.lastDecodedFrame == nil {
|
||||
self.accumulatedNalData = Data()
|
||||
guard let _ = try? surface.advanceFrame(sink: { [weak self] data in
|
||||
self?.accumulatedNalData.append(data)
|
||||
}) else {
|
||||
return false
|
||||
}
|
||||
if !self.accumulatedNalData.isEmpty {
|
||||
if let frames = try? decoder.decodeStream(self.accumulatedNalData) {
|
||||
self.lastDecodedFrame = frames.last
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let decodedFrame = self.lastDecodedFrame else {
|
||||
return false
|
||||
}
|
||||
|
||||
self.accumulatedNalData = Data()
|
||||
let yData = decodedFrame.y
|
||||
let cbData = decodedFrame.cb
|
||||
let crData = decodedFrame.cr
|
||||
let decodedWidth = Int(decodedFrame.width)
|
||||
let decodedHeight = Int(decodedFrame.height)
|
||||
let chromaWidth = decodedWidth / 2
|
||||
|
||||
guard let resizeResult = try? surface.resize(
|
||||
toMaxSlots: Int32(newMaxSlots),
|
||||
yPlane: yData,
|
||||
cbPlane: cbData,
|
||||
crPlane: crData,
|
||||
decodedWidth: Int32(decodedWidth),
|
||||
decodedHeight: Int32(decodedHeight),
|
||||
strideY: Int32(decodedWidth),
|
||||
strideCb: Int32(chromaWidth),
|
||||
strideCr: Int32(chromaWidth),
|
||||
withSink: { [weak self] data in
|
||||
self?.accumulatedNalData.append(data)
|
||||
}
|
||||
) else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.accumulatedNalData.isEmpty {
|
||||
if let frames = try? decoder.decodeStream(self.accumulatedNalData) {
|
||||
self.lastDecodedFrame = frames.last
|
||||
}
|
||||
}
|
||||
|
||||
var updatedContexts: [Int: SpriteContext] = [:]
|
||||
for newRegion in resizeResult.regions {
|
||||
let newSlot = Int(newRegion.slot)
|
||||
for (_, context) in self.spriteContexts {
|
||||
if updatedContexts.values.contains(where: { $0 === context }) {
|
||||
continue
|
||||
}
|
||||
let updated = SpriteContext(mbsPath: context.mbsPath, region: newRegion, metadata: context.metadata)
|
||||
updated.currentFrameIndex = context.currentFrameIndex
|
||||
updated.remainingDuration = context.remainingDuration
|
||||
for target in context.targets.copyItems() {
|
||||
if let target = target.value {
|
||||
let _ = updated.targets.add(Weak(target))
|
||||
}
|
||||
}
|
||||
updatedContexts[newSlot] = updated
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
self.spriteContexts = updatedContexts
|
||||
self.currentMaxSlots = newMaxSlots
|
||||
self.accumulatedNalData = Data()
|
||||
return true
|
||||
}
|
||||
|
||||
func tick(advanceTimestamp: Double) {
|
||||
guard let surface = self.surface, let decoder = self.decoder else {
|
||||
return
|
||||
}
|
||||
|
||||
var anyAdvanced = false
|
||||
for (slot, context) in self.spriteContexts {
|
||||
guard context.isPlaying else {
|
||||
continue
|
||||
}
|
||||
if advanceTimestamp > 0.0 {
|
||||
context.remainingDuration -= advanceTimestamp
|
||||
if context.remainingDuration <= 0.0 {
|
||||
surface.advanceSprite(atSlot: Int32(slot))
|
||||
context.currentFrameIndex += 1
|
||||
if context.currentFrameIndex >= context.metadata.frameCount {
|
||||
context.currentFrameIndex = 0
|
||||
}
|
||||
if context.currentFrameIndex < context.metadata.frameDurations.count {
|
||||
context.remainingDuration = context.metadata.frameDurations[context.currentFrameIndex]
|
||||
}
|
||||
anyAdvanced = true
|
||||
}
|
||||
} else {
|
||||
surface.advanceSprite(atSlot: Int32(slot))
|
||||
anyAdvanced = true
|
||||
}
|
||||
}
|
||||
|
||||
if !anyAdvanced {
|
||||
return
|
||||
}
|
||||
|
||||
self.accumulatedNalData = Data()
|
||||
guard let _ = try? surface.emitFrameIfNeeded(sink: { [weak self] data in
|
||||
self?.accumulatedNalData.append(data)
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !self.accumulatedNalData.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let frames = try? decoder.decodeStream(self.accumulatedNalData) else {
|
||||
return
|
||||
}
|
||||
guard let decodedFrame = frames.last else {
|
||||
return
|
||||
}
|
||||
|
||||
self.lastDecodedFrame = decodedFrame
|
||||
|
||||
for (_, context) in self.spriteContexts {
|
||||
let region = context.region
|
||||
let colorRect = region.colorRect
|
||||
let alphaRect = region.alphaRect
|
||||
|
||||
let frameWidth = Int(colorRect.width)
|
||||
let frameHeight = Int(colorRect.height)
|
||||
guard frameWidth > 0 && frameHeight > 0 else {
|
||||
continue
|
||||
}
|
||||
|
||||
let cgImage = extractCGImage(
|
||||
decodedFrame: decodedFrame,
|
||||
colorRect: colorRect,
|
||||
alphaRect: alphaRect,
|
||||
frameWidth: frameWidth,
|
||||
frameHeight: frameHeight
|
||||
)
|
||||
|
||||
let didLoop = context.currentFrameIndex == 0 && context.metadata.frameCount > 1
|
||||
|
||||
for targetRef in context.targets.copyItems() {
|
||||
if let target = targetRef.value {
|
||||
if let cgImage = cgImage {
|
||||
target.transitionToContents(cgImage, didLoop: didLoop)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return self.spriteContexts.isEmpty
|
||||
}
|
||||
|
||||
var hasPlayingSprites: Bool {
|
||||
for (_, context) in self.spriteContexts {
|
||||
if context.isPlaying {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func extractCGImage(
|
||||
decodedFrame: SCDecodedFrame,
|
||||
colorRect: CGRect,
|
||||
alphaRect: CGRect,
|
||||
frameWidth: Int,
|
||||
frameHeight: Int
|
||||
) -> CGImage? {
|
||||
let decodedWidth = Int(decodedFrame.width)
|
||||
let chromaWidth = decodedWidth / 2
|
||||
|
||||
let bytesPerRow = frameWidth * 4
|
||||
let bufferSize = frameHeight * bytesPerRow
|
||||
guard let buffer = malloc(bufferSize) else {
|
||||
return nil
|
||||
}
|
||||
let outPtr = buffer.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
let colorX = Int(colorRect.origin.x)
|
||||
let colorY = Int(colorRect.origin.y)
|
||||
let alphaX = Int(alphaRect.origin.x)
|
||||
let alphaY = Int(alphaRect.origin.y)
|
||||
|
||||
decodedFrame.y.withUnsafeBytes { yBuf in
|
||||
decodedFrame.cb.withUnsafeBytes { cbBuf in
|
||||
decodedFrame.cr.withUnsafeBytes { crBuf in
|
||||
let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
for row in 0 ..< frameHeight {
|
||||
for col in 0 ..< frameWidth {
|
||||
let srcY = yPtr[(colorY + row) * decodedWidth + (colorX + col)]
|
||||
let srcCb = cbPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)]
|
||||
let srcCr = crPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)]
|
||||
let srcA = yPtr[(alphaY + row) * decodedWidth + (alphaX + col)]
|
||||
|
||||
let yVal = Int(srcY) - 16
|
||||
let cbVal = Int(srcCb) - 128
|
||||
let crVal = Int(srcCr) - 128
|
||||
|
||||
var r = (298 * yVal + 459 * crVal + 128) >> 8
|
||||
var g = (298 * yVal - 55 * cbVal - 136 * crVal + 128) >> 8
|
||||
var b = (298 * yVal + 541 * cbVal + 128) >> 8
|
||||
|
||||
r = max(0, min(255, r))
|
||||
g = max(0, min(255, g))
|
||||
b = max(0, min(255, b))
|
||||
|
||||
let a = Int(srcA)
|
||||
let pr = (r * a + 127) / 255
|
||||
let pg = (g * a + 127) / 255
|
||||
let pb = (b * a + 127) / 255
|
||||
|
||||
let outOffset = (row * frameWidth + col) * 4
|
||||
outPtr[outOffset + 0] = UInt8(pb)
|
||||
outPtr[outOffset + 1] = UInt8(pg)
|
||||
outPtr[outOffset + 2] = UInt8(pr)
|
||||
outPtr[outOffset + 3] = UInt8(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let dataProvider = CGDataProvider(dataInfo: nil, data: buffer, size: bufferSize, releaseData: { _, data, _ in
|
||||
free(UnsafeMutableRawPointer(mutating: data))
|
||||
}) else {
|
||||
free(buffer)
|
||||
return nil
|
||||
}
|
||||
|
||||
return CGImage(
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 32,
|
||||
bytesPerRow: bytesPerRow,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue),
|
||||
provider: dataProvider,
|
||||
decode: nil,
|
||||
shouldInterpolate: false,
|
||||
intent: .defaultIntent
|
||||
)
|
||||
}
|
||||
|
||||
public final class SubcodecMultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
private struct SizeKey: Hashable {
|
||||
let width: Int
|
||||
let height: Int
|
||||
}
|
||||
|
||||
private var surfaceGroups: [SizeKey: SurfaceGroup] = [:]
|
||||
private var frameSkip: Int
|
||||
private var displayTimer: Foundation.Timer?
|
||||
|
||||
private(set) var isPlaying: Bool = false {
|
||||
didSet {
|
||||
if self.isPlaying != oldValue {
|
||||
if self.isPlaying {
|
||||
if self.displayTimer == nil {
|
||||
final class TimerTarget: NSObject {
|
||||
private let f: () -> Void
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
@objc func timerEvent() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
let frameInterval = Double(self.frameSkip) / 60.0
|
||||
let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.animationTick(frameInterval: frameInterval)
|
||||
}, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true)
|
||||
self.displayTimer = displayTimer
|
||||
RunLoop.main.add(displayTimer, forMode: .common)
|
||||
}
|
||||
} else {
|
||||
if let displayTimer = self.displayTimer {
|
||||
self.displayTimer = nil
|
||||
displayTimer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 {
|
||||
self.frameSkip = 1
|
||||
} else {
|
||||
self.frameSkip = 2
|
||||
}
|
||||
}
|
||||
|
||||
public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable {
|
||||
guard let subcodecCache = cache as? SubcodecAnimationCacheImpl else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let spriteWidth = roundUp(Int(size.width), multiple: 16)
|
||||
let spriteHeight = roundUp(Int(size.height), multiple: 16)
|
||||
let sizeKey = SizeKey(width: spriteWidth, height: spriteHeight)
|
||||
|
||||
let fetchDisposable = MetaDisposable()
|
||||
|
||||
fetchDisposable.set(subcodecCache.fetchIfNeeded(sourceId: itemId, size: size, fetch: fetch, completion: { [weak self, weak target] mbsPath in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self, let target = target, let mbsPath = mbsPath else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let metadata = subcodecCache.loadMetadata(sourceId: itemId, size: size) else {
|
||||
return
|
||||
}
|
||||
|
||||
let surfaceGroup: SurfaceGroup
|
||||
if let existing = strongSelf.surfaceGroups[sizeKey] {
|
||||
surfaceGroup = existing
|
||||
} else {
|
||||
surfaceGroup = SurfaceGroup(spriteWidth: spriteWidth, spriteHeight: spriteHeight)
|
||||
strongSelf.surfaceGroups[sizeKey] = surfaceGroup
|
||||
}
|
||||
|
||||
guard let spriteContext = surfaceGroup.addSprite(mbsPath: mbsPath, metadata: metadata) else {
|
||||
return
|
||||
}
|
||||
|
||||
let targetIndex = spriteContext.targets.add(Weak(target))
|
||||
target.numFrames = metadata.frameCount
|
||||
|
||||
let slot = Int(spriteContext.region.slot)
|
||||
|
||||
let deinitIndex = target.deinitCallbacks.add { [weak self, weak surfaceGroup] in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self, let surfaceGroup = surfaceGroup else {
|
||||
return
|
||||
}
|
||||
spriteContext.targets.remove(targetIndex)
|
||||
if spriteContext.targets.isEmpty {
|
||||
surfaceGroup.removeSprite(slot: slot)
|
||||
if surfaceGroup.isEmpty {
|
||||
strongSelf.surfaceGroups.removeValue(forKey: sizeKey)
|
||||
}
|
||||
strongSelf.updateIsPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let updateStateIndex = target.updateStateCallbacks.add { [weak self] in
|
||||
self?.updateIsPlaying()
|
||||
}
|
||||
|
||||
fetchDisposable.set(ActionDisposable { [weak self, weak surfaceGroup, weak target] in
|
||||
guard let strongSelf = self, let surfaceGroup = surfaceGroup else {
|
||||
return
|
||||
}
|
||||
if let target = target {
|
||||
target.deinitCallbacks.remove(deinitIndex)
|
||||
target.updateStateCallbacks.remove(updateStateIndex)
|
||||
}
|
||||
spriteContext.targets.remove(targetIndex)
|
||||
if spriteContext.targets.isEmpty {
|
||||
surfaceGroup.removeSprite(slot: slot)
|
||||
if surfaceGroup.isEmpty {
|
||||
strongSelf.surfaceGroups.removeValue(forKey: sizeKey)
|
||||
}
|
||||
strongSelf.updateIsPlaying()
|
||||
}
|
||||
})
|
||||
|
||||
strongSelf.updateIsPlaying()
|
||||
}
|
||||
}))
|
||||
|
||||
return ActionDisposable {
|
||||
fetchDisposable.dispose()
|
||||
}.strict()
|
||||
}
|
||||
|
||||
public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
|
||||
if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) {
|
||||
guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else {
|
||||
return false
|
||||
}
|
||||
switch frame.frame.format {
|
||||
case let .rgba(data, width, height, bytesPerRow):
|
||||
guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else {
|
||||
return false
|
||||
}
|
||||
data.withUnsafeBytes { bytes -> Void in
|
||||
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
|
||||
}
|
||||
guard let image = context.generateImage() else {
|
||||
return false
|
||||
}
|
||||
target.contents = image.cgImage
|
||||
target.numFrames = item.numFrames
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable {
|
||||
return cache.getFirstFrame(queue: .mainQueue(), sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] result in
|
||||
guard let item = result.item else {
|
||||
Queue.mainQueue().async {
|
||||
completion(false, result.isFinal)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let loaded: Bool
|
||||
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
|
||||
switch frame.frame.format {
|
||||
case let .rgba(data, width, height, bytesPerRow):
|
||||
Queue.mainQueue().async {
|
||||
guard let target = target else {
|
||||
completion(false, true)
|
||||
return
|
||||
}
|
||||
target.numFrames = item.numFrames
|
||||
if let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) {
|
||||
data.withUnsafeBytes { bytes -> Void in
|
||||
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
|
||||
}
|
||||
if let image = context.generateImage() {
|
||||
target.contents = image.cgImage
|
||||
completion(true, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
completion(false, true)
|
||||
}
|
||||
return
|
||||
default:
|
||||
loaded = false
|
||||
}
|
||||
} else {
|
||||
loaded = false
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(loaded, true)
|
||||
}
|
||||
}).strict()
|
||||
}
|
||||
|
||||
public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable {
|
||||
return cache.getFirstFrame(queue: .mainQueue(), sourceId: itemId, size: size, fetch: fetch, completion: { result in
|
||||
guard let item = result.item else {
|
||||
Queue.mainQueue().async {
|
||||
completion(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
|
||||
switch frame.frame.format {
|
||||
case let .rgba(data, width, height, bytesPerRow):
|
||||
Queue.mainQueue().async {
|
||||
if let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) {
|
||||
data.withUnsafeBytes { bytes -> Void in
|
||||
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
|
||||
}
|
||||
completion(context.generateImage()?.cgImage)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(nil)
|
||||
}
|
||||
}).strict()
|
||||
}
|
||||
|
||||
public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) {
|
||||
}
|
||||
|
||||
private func updateIsPlaying() {
|
||||
var isPlaying = false
|
||||
for (_, group) in self.surfaceGroups {
|
||||
if group.hasPlayingSprites {
|
||||
isPlaying = true
|
||||
break
|
||||
}
|
||||
}
|
||||
self.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
private func animationTick(frameInterval: Double) {
|
||||
for (_, group) in self.surfaceGroups {
|
||||
if group.hasPlayingSprites {
|
||||
group.tick(advanceTimestamp: frameInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ import FetchManagerImpl
|
|||
import InAppPurchaseManager
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import DCTAnimationCacheImpl
|
||||
import DCTMultiAnimationRendererImpl
|
||||
import AppBundle
|
||||
import DirectMediaImageCache
|
||||
|
||||
|
|
@ -317,15 +319,15 @@ public final class AccountContextImpl: AccountContext {
|
|||
self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl()
|
||||
|
||||
let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox
|
||||
self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
self.animationCache = DCTAnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
return TempBox.shared.tempFile(fileName: "file").path
|
||||
}, updateStorageStats: { path, size in
|
||||
if let pathData = path.data(using: .utf8) {
|
||||
cacheStorageBox.update(id: pathData, size: size)
|
||||
}
|
||||
})
|
||||
self.animationRenderer = MultiAnimationRendererImpl()
|
||||
(self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache
|
||||
self.animationRenderer = DCTMultiAnimationRendererImpl()
|
||||
(self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache
|
||||
|
||||
let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration])
|
||||
|> map { preferences -> LimitsConfiguration in
|
||||
|
|
@ -496,7 +498,7 @@ public final class AccountContextImpl: AccountContext {
|
|||
guard let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) else {
|
||||
return
|
||||
}
|
||||
(self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache
|
||||
(self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -445,6 +445,22 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
|
|||
}
|
||||
|
||||
if preload {
|
||||
var channelMessage: Signal<Void, NoError> = .single(Void())
|
||||
if case .channelPost = subject, let channelMessageId = replyThreadMessage.channelMessageId {
|
||||
channelMessage = context.engine.messages.getMessagesLoadIfNecessary([channelMessageId], strategy: .cloud(skipLocal: false))
|
||||
|> mapToSignal { result -> Signal<Void, GetMessagesError> in
|
||||
switch result {
|
||||
case .progress:
|
||||
return .never()
|
||||
case .result:
|
||||
return .single(Void())
|
||||
}
|
||||
}
|
||||
|> `catch` { _ -> Signal<Void, NoError> in
|
||||
return .single(Void())
|
||||
}
|
||||
}
|
||||
|
||||
let preloadSignal = preloadedChatHistoryViewForLocation(
|
||||
input,
|
||||
context: context,
|
||||
|
|
@ -455,8 +471,8 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
|
|||
tag: nil,
|
||||
additionalData: []
|
||||
)
|
||||
return preloadSignal
|
||||
|> map { historyView -> Bool? in
|
||||
return combineLatest(preloadSignal, channelMessage)
|
||||
|> map { historyView, _ -> Bool? in
|
||||
switch historyView {
|
||||
case .Loading:
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -600,9 +600,12 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
|||
return .peer(.name(pathComponents[1]), .boost)
|
||||
} else if pathComponents[0] == "giftcode", pathComponents.count == 2 {
|
||||
return .premiumGiftCode(slug: pathComponents[1])
|
||||
} else if pathComponents.count >= 3 && pathComponents[0] == "newbot" {
|
||||
} else if pathComponents.count >= 2 && pathComponents[0] == "newbot" {
|
||||
let parentBot = pathComponents[1]
|
||||
let username = pathComponents[2]
|
||||
var username: String?
|
||||
if pathComponents.count >= 3 {
|
||||
username = pathComponents[2]
|
||||
}
|
||||
var title: String?
|
||||
for queryItem in components.queryItems ?? [] {
|
||||
if let value = queryItem.value {
|
||||
|
|
|
|||
69
third-party/openh264/BUILD
vendored
69
third-party/openh264/BUILD
vendored
|
|
@ -267,3 +267,72 @@ cc_library(
|
|||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "openh264_decoder",
|
||||
srcs = [
|
||||
"third_party/openh264/src/codec/decoder/core/src/au_parser.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/bit_stream.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/cabac_decoder.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/deblocking.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/decode_mb_aux.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/decode_slice.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/decoder.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/decoder_core.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/decoder_data_tables.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/error_concealment.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/fmo.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/get_intra_predictor.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/manage_dec_ref.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/memmgr_nal_unit.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/mv_pred.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/parse_mb_syn_cabac.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/parse_mb_syn_cavlc.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/pic_queue.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/rec_mb.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/src/wels_decoder_thread.cpp",
|
||||
"third_party/openh264/src/codec/decoder/plus/src/welsDecoderExt.cpp",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/au_parser.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/bit_stream.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/cabac_decoder.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/deblocking.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/dec_frame.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/dec_golomb.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/decode_mb_aux.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/decode_slice.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/decoder.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/decoder_context.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/decoder_core.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/error_code.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/error_concealment.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/fmo.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/get_intra_predictor.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/manage_dec_ref.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/mb_cache.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/memmgr_nal_unit.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/mv_pred.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/nal_prefix.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/nalu.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/parameter_sets.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/parse_mb_syn_cabac.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/parse_mb_syn_cavlc.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/pic_queue.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/picture.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/rec_mb.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/slice.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/vlc_decoder.h",
|
||||
"third_party/openh264/src/codec/decoder/core/inc/wels_common_basis.h",
|
||||
"third_party/openh264/src/codec/decoder/plus/inc/welsDecoderExt.h",
|
||||
],
|
||||
includes = ["."],
|
||||
copts = [
|
||||
"-Ithird-party/openh264/third_party/openh264/src/codec/decoder/core/inc",
|
||||
"-Ithird-party/openh264/third_party/openh264/src/codec/decoder/plus/inc",
|
||||
"-Ithird-party/openh264/third_party/openh264/src/codec/common/inc",
|
||||
"-Ithird-party/openh264/third_party/openh264/src/codec/api/wels",
|
||||
"-Os",
|
||||
"-Wno-all",
|
||||
],
|
||||
deps = [":openh264"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -313,7 +313,10 @@ typedef enum {
|
|||
LEVEL_4_2 = 42,
|
||||
LEVEL_5_0 = 50,
|
||||
LEVEL_5_1 = 51,
|
||||
LEVEL_5_2 = 52
|
||||
LEVEL_5_2 = 52,
|
||||
LEVEL_6_0 = 60,
|
||||
LEVEL_6_1 = 61,
|
||||
LEVEL_6_2 = 62
|
||||
} ELevelIdc;
|
||||
|
||||
/**
|
||||
|
|
@ -592,6 +595,7 @@ typedef struct TagEncParamExt {
|
|||
bool bIsLosslessLink; ///< LTR advanced setting
|
||||
bool bFixRCOverShoot; ///< fix rate control overshooting
|
||||
int iIdrBitrateRatio; ///< the target bits of IDR is (idr_bitrate_ratio/100) * average target bit per frame.
|
||||
bool bSubcodecMode; ///< enable subcodec sprite-compositing constraints (MV cap, no intra in P-frames, no sub-partitions, padding recon override, reduced log2_max_frame_num); default false
|
||||
} SEncParamExt;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace WelsCommon {
|
|||
|
||||
#define CTX_NA 0
|
||||
#define WELS_CONTEXT_COUNT 460
|
||||
#define LEVEL_NUMBER 17
|
||||
#define LEVEL_NUMBER 20
|
||||
typedef struct TagLevelLimits {
|
||||
ELevelIdc uiLevelIdc; // level idc
|
||||
uint32_t uiMaxMBPS; // Max macroblock processing rate(MB/s)
|
||||
|
|
|
|||
|
|
@ -363,10 +363,14 @@ const SLevelLimits g_ksLevelLimits[LEVEL_NUMBER] = {
|
|||
|
||||
{LEVEL_5_0, 589824, 22080, 110400, 135000, 135000, -2048, 2047, 2, 16}, /* level 5 */
|
||||
{LEVEL_5_1, 983040, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16}, /* level 5.1 */
|
||||
{LEVEL_5_2, 2073600, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16} /* level 5.2 */
|
||||
{LEVEL_5_2, 2073600, 36864, 184320, 240000, 240000, -2048, 2047, 2, 16}, /* level 5.2 */
|
||||
|
||||
{LEVEL_6_0, 4177920, 139264, 696320, 240000, 240000, -8192, 8191, 2, 16}, /* level 6.0 */
|
||||
{LEVEL_6_1, 8355840, 139264, 696320, 480000, 480000, -8192, 8191, 2, 16}, /* level 6.1 */
|
||||
{LEVEL_6_2, 16711680, 139264, 696320, 800000, 800000, -8192, 8191, 2, 16} /* level 6.2 */
|
||||
};
|
||||
const uint32_t g_kuiLevelMaps[LEVEL_NUMBER] = {
|
||||
10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52
|
||||
10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52, 60, 61, 62
|
||||
};
|
||||
//for cabac
|
||||
/* this table is from Table9-12 to Table 9-24 */
|
||||
|
|
|
|||
|
|
@ -829,6 +829,12 @@ const SLevelLimits* GetLevelLimits (int32_t iLevelIdx, bool bConstraint3) {
|
|||
return &g_ksLevelLimits[15];
|
||||
case 52:
|
||||
return &g_ksLevelLimits[16];
|
||||
case 60:
|
||||
return &g_ksLevelLimits[17];
|
||||
case 61:
|
||||
return &g_ksLevelLimits[18];
|
||||
case 62:
|
||||
return &g_ksLevelLimits[19];
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ int32_t WelsWritePpsSyntax (SWelsPPS* pPps, SBitStringAux* pBitStringAux, IWelsP
|
|||
int32_t WelsInitSps (SWelsSPS* pSps, SSpatialLayerConfig* pLayerParam, SSpatialLayerInternal* pLayerParamInternal,
|
||||
const uint32_t kuiIntraPeriod, const int32_t kiNumRefFrame,
|
||||
const uint32_t kiSpsId, const bool kbEnableFrameCropping, bool bEnableRc,
|
||||
const int32_t kiDlayerCount,bool bSVCBaselayer);
|
||||
const int32_t kiDlayerCount, bool bSVCBaselayer, bool bSubcodecMode);
|
||||
|
||||
/*!
|
||||
* \brief initialize subset pSps based on configurable parameters in svc
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@ typedef struct TagWelsSvcCodingParam: SEncParamExt {
|
|||
bIsLosslessLink = pCodingParam.bIsLosslessLink;
|
||||
bFixRCOverShoot = pCodingParam.bFixRCOverShoot;
|
||||
iIdrBitrateRatio = pCodingParam.iIdrBitrateRatio;
|
||||
bSubcodecMode = pCodingParam.bSubcodecMode;
|
||||
if (iUsageType == SCREEN_CONTENT_REAL_TIME && !bIsLosslessLink && bEnableLongTermReference) {
|
||||
bEnableLongTermReference = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -485,14 +485,14 @@ static inline bool WelsGetPaddingOffset (int32_t iActualWidth, int32_t iActualHe
|
|||
int32_t WelsInitSps (SWelsSPS* pSps, SSpatialLayerConfig* pLayerParam, SSpatialLayerInternal* pLayerParamInternal,
|
||||
const uint32_t kuiIntraPeriod, const int32_t kiNumRefFrame,
|
||||
const uint32_t kuiSpsId, const bool kbEnableFrameCropping, bool bEnableRc,
|
||||
const int32_t kiDlayerCount, bool bSVCBaselayer) {
|
||||
const int32_t kiDlayerCount, bool bSVCBaselayer, bool bSubcodecMode) {
|
||||
memset (pSps, 0, sizeof (SWelsSPS));
|
||||
pSps->uiSpsId = kuiSpsId;
|
||||
pSps->iMbWidth = (pLayerParam->iVideoWidth + 15) >> 4;
|
||||
pSps->iMbHeight = (pLayerParam->iVideoHeight + 15) >> 4;
|
||||
|
||||
//max value of both iFrameNum and POC are 2^16-1, in our encoder, iPOC=2*iFrameNum, so max of iFrameNum should be 2^15-1.--
|
||||
pSps->uiLog2MaxFrameNum = 15;//16;
|
||||
pSps->uiLog2MaxFrameNum = bSubcodecMode ? 4 : 15;//16;
|
||||
pSps->iLog2MaxPocLsb = 1 + pSps->uiLog2MaxFrameNum;
|
||||
|
||||
pSps->iNumRefFrames = kiNumRefFrame; /* min pRef size when fifo pRef operation*/
|
||||
|
|
@ -565,7 +565,7 @@ int32_t WelsInitSubsetSps (SSubsetSps* pSubsetSps, SSpatialLayerConfig* pLayerPa
|
|||
memset (pSubsetSps, 0, sizeof (SSubsetSps));
|
||||
|
||||
WelsInitSps (pSps, pLayerParam, pLayerParamInternal, kuiIntraPeriod, kiNumRefFrame, kuiSpsId, kbEnableFrameCropping,
|
||||
bEnableRc, kiDlayerCount, false);
|
||||
bEnableRc, kiDlayerCount, false, false);
|
||||
|
||||
pSps->uiProfileIdc = pLayerParam->uiProfileIdc ;
|
||||
|
||||
|
|
|
|||
|
|
@ -1526,6 +1526,13 @@ void GetMvMvdRange (SWelsSvcCodingParam* pParam, int32_t& iMvRange, int32_t& iMv
|
|||
|
||||
iMvRange = WELS_MIN (iMvRange, iFixMvRange);
|
||||
|
||||
// [subcodec] Limit MV range to 16 pixels for sprite compositing.
|
||||
// Sprites use a 16px padded canvas; tighter MV range prevents referencing
|
||||
// pixels outside the padding that will differ in the composite frame.
|
||||
if (pParam->bSubcodecMode) {
|
||||
iMvRange = WELS_MIN (iMvRange, 16);
|
||||
}
|
||||
|
||||
iMvdRange = (iMvRange + 1) << 1;
|
||||
|
||||
iMvdRange = WELS_MIN (iMvdRange, iFixMvdRange);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ static int32_t WelsGenerateNewSps (sWelsEncCtx* pCtx, const bool kbUseSubsetSps,
|
|||
iRet = WelsInitSps (pSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod,
|
||||
pParam->iMaxNumRefFrame,
|
||||
kiSpsId, pParam->bEnableFrameCroppingFlag, pParam->iRCMode != RC_OFF_MODE, iDlayerCount,
|
||||
bSVCBaselayer);
|
||||
bSVCBaselayer, pParam->bSubcodecMode);
|
||||
} else {
|
||||
iRet = WelsInitSubsetSps (pSubsetSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod,
|
||||
pParam->iMaxNumRefFrame,
|
||||
|
|
@ -178,7 +178,7 @@ int32_t FindExistingSps (SWelsSvcCodingParam* pParam, const bool kbUseSubsetSps,
|
|||
WelsInitSps (&sTmpSps, pDlayerParam, &pParam->sDependencyLayers[iDlayerIndex], pParam->uiIntraPeriod,
|
||||
pParam->iMaxNumRefFrame,
|
||||
0, pParam->bEnableFrameCroppingFlag, pParam->iRCMode != RC_OFF_MODE, iDlayerCount,
|
||||
bSVCBaseLayer);
|
||||
bSVCBaseLayer, pParam->bSubcodecMode);
|
||||
for (int32_t iId = 0; iId < iSpsNumInUse; iId++) {
|
||||
if (CheckMatchedSps (&sTmpSps, &pSpsArray[iId])) {
|
||||
return iId;
|
||||
|
|
|
|||
|
|
@ -930,6 +930,11 @@ int32_t WelsMdIntraChroma (SWelsFuncPtrList* pFunc, SDqLayer* pCurDqLayer, SMbCa
|
|||
return iBestCost;
|
||||
}
|
||||
int32_t WelsMdIntraFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) {
|
||||
// [subcodec] I_4x4 disabled for sprite compositing — only I_16x16 allowed.
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
return pWelsMd->iCostLuma;
|
||||
}
|
||||
|
||||
int32_t iCosti4x4 = WelsMdI4x4 (pEncCtx, pWelsMd, pCurMb, pMbCache);
|
||||
|
||||
if (iCosti4x4 < pWelsMd->iCostLuma) {
|
||||
|
|
@ -940,6 +945,10 @@ int32_t WelsMdIntraFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* p
|
|||
}
|
||||
|
||||
int32_t WelsMdIntraFinePartitionVaa (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) {
|
||||
// [subcodec] I_4x4 disabled for sprite compositing — only I_16x16 allowed.
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
return pWelsMd->iCostLuma;
|
||||
}
|
||||
|
||||
if (MdIntraAnalysisVaaInfo (pEncCtx, pMbCache->SPicData.pEncMb[0])) {
|
||||
int32_t iCosti4x4 = WelsMdI4x4Fast (pEncCtx, pWelsMd, pCurMb, pMbCache);
|
||||
|
|
@ -1236,6 +1245,11 @@ int32_t WelsMdP4x8 (SWelsFuncPtrList* pFunc, SDqLayer* pCurDqLayer, SWelsMD* pWe
|
|||
}
|
||||
|
||||
void WelsMdInterFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb, int32_t iBestCost) {
|
||||
// [subcodec] No sub-partition modes for sprite compositing.
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer;
|
||||
// SMbCache *pMbCache = &pSlice->sMbCacheInfo;
|
||||
int32_t iCost = 0;
|
||||
|
|
@ -1269,6 +1283,11 @@ void WelsMdInterFinePartition (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* p
|
|||
|
||||
void WelsMdInterFinePartitionVaa (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb,
|
||||
int32_t iBestCost) {
|
||||
// [subcodec] No sub-partition modes for sprite compositing.
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer;
|
||||
// SMbCache *pMbCache = &pSlice->sMbCacheInfo;
|
||||
int32_t iCostP8x16, iCostP16x8, iCostP8x8;
|
||||
|
|
@ -1827,6 +1846,11 @@ void WelsMdInterMbRefinement (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurM
|
|||
|
||||
}
|
||||
bool WelsMdFirstIntraMode (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SMB* pCurMb, SMbCache* pMbCache) {
|
||||
// [subcodec] Never select intra MBs in P-frames for sprite compositing.
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SWelsFuncPtrList* pFunc = pEncCtx->pFuncList;
|
||||
|
||||
int32_t iCostI16x16 = WelsMdI16x16 (pFunc, pEncCtx->pCurDqLayer, pMbCache, pWelsMd->iLambda);
|
||||
|
|
|
|||
|
|
@ -564,6 +564,33 @@ TRY_REENCODING:
|
|||
if (ENC_RETURN_SUCCESS != iEncReturn)
|
||||
return iEncReturn;
|
||||
|
||||
// [subcodec] For padding MBs, overwrite reconstruction buffer with exact black.
|
||||
// This ensures content MBs' I_16x16 predictions are computed against exact-black
|
||||
// neighbors, making their coefficients transplantable to the composite frame.
|
||||
{
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
const int32_t kiMbX = iCurMbIdx % pCurLayer->iMbWidth;
|
||||
const int32_t kiMbY = iCurMbIdx / pCurLayer->iMbWidth;
|
||||
if (kiMbX < 1 || kiMbX >= pCurLayer->iMbWidth - 1 ||
|
||||
kiMbY < 1 || kiMbY >= pCurLayer->iMbHeight - 1) {
|
||||
// Overwrite luma reconstruction to black (Y=0)
|
||||
uint8_t* pCsY = pMbCache->SPicData.pCsMb[0];
|
||||
const int32_t kiCsStrideY = pCurLayer->iCsStride[0];
|
||||
for (int32_t r = 0; r < MB_WIDTH_LUMA; r++) {
|
||||
memset(pCsY + r * kiCsStrideY, 0, MB_WIDTH_LUMA);
|
||||
}
|
||||
// Overwrite chroma reconstruction to neutral (Cb=128, Cr=128)
|
||||
uint8_t* pCsCb = pMbCache->SPicData.pCsMb[1];
|
||||
uint8_t* pCsCr = pMbCache->SPicData.pCsMb[2];
|
||||
const int32_t kiCsStrideUV = pCurLayer->iCsStride[1];
|
||||
for (int32_t r = 0; r < MB_WIDTH_CHROMA; r++) {
|
||||
memset(pCsCb + r * kiCsStrideUV, 128, MB_WIDTH_CHROMA);
|
||||
memset(pCsCr + r * kiCsStrideUV, 128, MB_WIDTH_CHROMA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pCurMb->uiSliceIdc = kiSliceIdx;
|
||||
|
||||
#if defined(MB_TYPES_CHECK)
|
||||
|
|
|
|||
|
|
@ -611,6 +611,11 @@ bool TryModeMerge (SMbCache* pMbCache, SWelsMD* pWelsMd, SMB* pCurMb) {
|
|||
|
||||
void WelsMdInterFinePartitionVaaOnScreen (sWelsEncCtx* pEncCtx, SWelsMD* pWelsMd, SSlice* pSlice, SMB* pCurMb,
|
||||
int32_t iBestCost) {
|
||||
// [subcodec] No sub-partition modes for sprite compositing.
|
||||
if (pEncCtx->pSvcParam->bSubcodecMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
SMbCache* pMbCache = &pSlice->sMbCacheInfo;
|
||||
SDqLayer* pCurDqLayer = pEncCtx->pCurDqLayer;
|
||||
int32_t iCostP8x8;
|
||||
|
|
|
|||
11
third-party/subcodec/.gitignore
vendored
Normal file
11
third-party/subcodec/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
build/
|
||||
build-*/
|
||||
build_*/
|
||||
.build/
|
||||
.swiftpm/
|
||||
*.h264
|
||||
*.yuv
|
||||
*.mbs
|
||||
*.trace/
|
||||
Tests/SubcodecTests/Fixtures/
|
||||
.DS_Store
|
||||
84
third-party/subcodec/BUILD
vendored
Normal file
84
third-party/subcodec/BUILD
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
cc_library(
|
||||
name = "h264bitstream",
|
||||
srcs = [
|
||||
"third_party/h264bitstream/h264_stream.c",
|
||||
"third_party/h264bitstream/h264_nal.c",
|
||||
"third_party/h264bitstream/h264_sei.c",
|
||||
],
|
||||
hdrs = glob(["third_party/h264bitstream/*.h"]),
|
||||
includes = ["third_party/h264bitstream"],
|
||||
copts = ["-Wno-all"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "subcodec",
|
||||
srcs = [
|
||||
"src/frame_writer.cpp",
|
||||
"src/cavlc.cpp",
|
||||
"src/h264_parser.cpp",
|
||||
"src/sprite_data.cpp",
|
||||
"src/mbs_encode.cpp",
|
||||
"src/mbs_mux_common.cpp",
|
||||
"src/mux_surface.cpp",
|
||||
],
|
||||
hdrs = glob(["src/*.h"]),
|
||||
includes = ["src"],
|
||||
copts = [
|
||||
"-std=c++2b",
|
||||
"-Wno-all",
|
||||
],
|
||||
deps = [":h264bitstream"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "sprite_encode",
|
||||
srcs = [
|
||||
"src/sprite_encode.cpp",
|
||||
"src/sprite_extractor.cpp",
|
||||
],
|
||||
hdrs = glob(["src/*.h"]),
|
||||
includes = ["src"],
|
||||
copts = [
|
||||
"-std=c++2b",
|
||||
"-Wno-all",
|
||||
"-Ithird-party/openh264/third_party/openh264/src/codec/api/wels",
|
||||
],
|
||||
deps = [
|
||||
":subcodec",
|
||||
"//third-party/openh264:openh264",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SubcodecObjC",
|
||||
module_name = "SubcodecObjC",
|
||||
enable_modules = True,
|
||||
srcs = [
|
||||
"Sources/SubcodecObjC/SCSprite.mm",
|
||||
"Sources/SubcodecObjC/SCMuxSurface.mm",
|
||||
"Sources/SubcodecObjC/SCSpriteRegion.mm",
|
||||
"Sources/SubcodecObjC/SCDecodedFrame.mm",
|
||||
"Sources/SubcodecObjC/SCOpenH264Decoder.mm",
|
||||
"Sources/SubcodecObjC/SCVideoToolboxDecoder.mm",
|
||||
"Sources/SubcodecObjC/AnnexBSplitter.h",
|
||||
],
|
||||
hdrs = glob(["Sources/SubcodecObjC/include/*.h"]),
|
||||
includes = ["Sources/SubcodecObjC/include"],
|
||||
copts = [
|
||||
"-std=c++2b",
|
||||
"-Wno-all",
|
||||
"-Ithird-party/openh264/third_party/openh264/src/codec/api/wels",
|
||||
],
|
||||
deps = [
|
||||
":subcodec",
|
||||
":sprite_encode",
|
||||
"//third-party/openh264:openh264",
|
||||
"//third-party/openh264:openh264_decoder",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"VideoToolbox",
|
||||
"CoreMedia",
|
||||
"CoreVideo",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
383
third-party/subcodec/CLAUDE.md
vendored
Normal file
383
third-party/subcodec/CLAUDE.md
vendored
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
# subcodec
|
||||
|
||||
C++23 library that constructs H.264 Baseline/High Profile bitstreams for sprite multiplexing. Composites many small sprite animations into a single H.264 stream decoded by one hardware decoder, using selective macroblock updates (P_16x16, I_16x16, skip) in P-frames.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cmake -B build && cmake --build build
|
||||
```
|
||||
|
||||
- This project lives inside `telegram-ios/third-party/subcodec/`
|
||||
- CMake 3.16+, C++23 standard (for `std::expected`); OpenH264 compiled as C++11
|
||||
- Dependencies: FFmpeg (libavcodec, libavformat, libavutil, libswscale) — required for sprite_extract video input; vendored h264bitstream; OpenH264 encoder + decoder (Telegram's patched copy at the sibling `../openh264/` directory, accessed via symlink `third_party/openh264_codec` — not vendored here)
|
||||
- Produces: `libsubcodec.a`, 25 test executables, `sprite_extract` tool, `sprite_mux` tool
|
||||
|
||||
### SwiftPM
|
||||
|
||||
```bash
|
||||
cmake --build build --target fixtures # generate YUV fixtures before running Swift tests
|
||||
swift build # builds core library + OpenH264 + sprite_encode + ObjC++ wrapper
|
||||
swift test # runs Swift XCTest suite (12 tests including 160-frame e2e)
|
||||
```
|
||||
|
||||
- Swift 5.9+, targets macOS 10.15+/iOS 13+/tvOS 13+
|
||||
- SwiftPM targets: `h264bitstream`, `subcodec`, `oh264_common`, `oh264_processing`, `oh264_encoder`, `oh264_decoder`, `sprite_encode`, `SubcodecObjC`, `SubcodecTests`
|
||||
- OpenH264 is referenced via the `third_party/openh264_codec` symlink pointing to Telegram's sibling `../openh264/` directory
|
||||
- CMake remains the primary build system; SwiftPM builds alongside it
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/types.h` — Core types: `MacroblockData`, `FrameParams`, `MbContext`, `MbsRow`, `MbsEncodedFrame` (merged rows only), `MbsFrame` (`merged_rows` span), `MbsSprite` (bulk data + row storage)
|
||||
- `src/error.h` — `subcodec::Error` enum class
|
||||
- `src/tables.h` — Compile-time lookup tables (CBP, block scan order)
|
||||
- `src/frame_writer.cpp/.h` — `subcodec::frame_writer` namespace: `write_headers` (auto-selects Baseline/High Profile + level from frame size), `write_idr_frame_ex`, `write_p_frame_ex`
|
||||
- `src/cavlc.cpp/.h` — `subcodec::cavlc` namespace: CAVLC entropy coding and decoding (spec tables, block read/write, nC context)
|
||||
- `src/h264_parser.cpp/.h` — `subcodec::H264Parser` class: H.264 Baseline CAVLC slice parser with RAII buffers
|
||||
- `src/mux_surface.cpp/.h` — `subcodec::MuxSurface` class: streaming mux surface with sprite loading/removal/looping, P-frame emission, dynamic resize. `Params` takes `sprite_width`/`sprite_height` in content pixels (padding added internally). `add_sprite` returns `SpriteRegion` (slot index + color/alpha content pixel rects in composite frame); also sets `needs_emit`/`dirty_` so the next `emit_frame_if_needed` emits the sprite's frame 0 introduction. `advance_sprite(slot)` schedules one sprite for emit+advance (sets `needs_emit` flag); `emit_frame_if_needed(sink)` emits a P-frame if any sprites are scheduled, then advances their frame indices (emit-then-advance semantics). `advance_frame(sink)` is a convenience wrapper that schedules all active sprites then calls `emit_frame_if_needed`. `resize` emits new SPS/PPS + all-I_PCM IDR to change grid dimensions mid-stream, compacting active sprites. `check_compaction_opportunity` returns `CompactionInfo` for caller-driven resize decisions
|
||||
- `src/mbs_mux_common.cpp/.h` — `subcodec::mux` namespace: shared muxing primitives, `EbspWriter` (single-pass direct EBSP output with inline escape checking + zero-run metadata fast path + `flush_bytes` for NEON-accelerated bulk byte writing), `RbspWriter` (branchless RBSP staging writer, no escape checking), `MicroOp` (pre-resolved blob operation), `RowOp`/`CompositeRowPlan` (precomputed composite grid layout), `build_row_plans`, `build_micro_ops`, `write_p_frame_micro` (active P-frame path), `write_p_frame_rbsp` (legacy two-pass path), `write_idr_ipcm` (inline in header — all-I_PCM IDR for resize transitions, single-pass EbspWriter with precomputed black-MB fast path), `write_skip_safe` (splits long mb_skip_runs with dummy P_16x16 zero-residual MBs for VT compatibility), `scan_zero_runs`, `rbsp_to_ebsp_neon`, exp-golomb LUT, row-blob copy, RBSP↔EBSP conversion
|
||||
- `src/mbs_encode.cpp/.h` — `subcodec::mbs` namespace: MBS binary format encoding with real nC context and zero-run scanning, returns `MbsEncodedFrame`
|
||||
- `src/mbs_format.h` — MBS binary format constants (MBS6 format). Header: magic `MBS6` (4 bytes) + width_mbs(2) + height_mbs(2) + num_frames(2) + qp(1) + qp_delta_idr(1) + qp_delta_p(1) + flags(1) = 14 bytes
|
||||
- `src/sprite_data.cpp` — `MbsSprite` file I/O (v6 bulk load/save with pre-merged blobs), `set_frames()` — no runtime merge needed
|
||||
- `src/sprite_encode.cpp/.h` — `subcodec::SpriteEncoder` class: OpenH264 encode + parse pipeline. `Params` takes content `width`/`height` in pixels (padding added internally). Encodes on a double-wide canvas (color left, alpha right) and returns `EncodeResult{color, alpha}`.
|
||||
- `src/sprite_extractor.cpp/.h` — `subcodec::SpriteExtractor` class: raw YUV+alpha → padded canvas → SpriteEncoder → .mbs file with pre-merged row blobs. `Params` takes content `sprite_size` in pixels (padding always 16px internally)
|
||||
- `tools/sprite_extract.cpp` — CLI: video file → `.mbs` via FFmpeg decode + SpriteEncoder
|
||||
- `tools/sprite_mux.cpp` — CLI: `.mbs` files → composite H.264 stream via MuxSurface
|
||||
- `third_party/h264bitstream/` — Vendored NAL unit / bitstream primitives (C)
|
||||
- `third_party/openh264_codec` — Symlink to Telegram's OpenH264 at the sibling `../openh264/` directory (not vendored here; patches documented below)
|
||||
- `test/` — 25 standalone C++ test programs (no framework)
|
||||
- `Sources/SubcodecObjC/` — ObjC++ wrapper: SCSprite, SCMuxSurface, SCSpriteRegion, SCResizeResult, SCCompactionInfo, SCOpenH264Decoder, SCVideoToolboxDecoder, SCDecodedFrame, SCDecoding protocol
|
||||
- `Sources/SpriteEncode/` — SwiftPM wrapper compilation units for sprite_encode.cpp/sprite_extractor.cpp
|
||||
- `Tests/SubcodecTests/` — Swift XCTest suite with YUV fixtures (3 sprites × 160 frames)
|
||||
- `docs/plans/` — Design documents
|
||||
- `docs/superpowers/specs/` — Feature specs
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
cd build && ctest
|
||||
```
|
||||
|
||||
Each test is a standalone C++ executable (no framework):
|
||||
- `test_cavlc` — CAVLC write encoding correctness
|
||||
- `test_cavlc_read` — CAVLC read/write round-trip across all 5 VLC tables
|
||||
- `test_cavlc_diag` — CAVLC diagnostic tests
|
||||
- `test_cavlc_split` — CAVLC split encoding tests
|
||||
- `test_ct_lut` — Coeff token lookup table tests
|
||||
- `test_mb_p16x16` — P_16x16 macroblock encoder + MV prediction
|
||||
- `test_mb_i16x16` — I_16x16 macroblock encoder
|
||||
- `test_p_frame_ex` — Extended P-frame writer (all MB types)
|
||||
- `test_h264_parse` — H.264 slice parser round-trip (write -> parse -> verify)
|
||||
- `test_idr_frame_ex` — Extended IDR frame writer round-trip
|
||||
- `test_mbs_format` — MBS binary format encoding/decoding
|
||||
- `test_mbs_encode` — MBS frame encoding
|
||||
- `test_mux` (requires OpenH264) — End-to-end: 4 sprites x 8 frames, encode -> mux_surface -> decode -> pixel-identical verification
|
||||
- `test_mux_surface` (requires OpenH264) — Streaming mux surface: staggered sprite add/remove, mid-stream I_16x16 introduction, sprite looping (2-cycle pixel-identical verification), pixel verification
|
||||
- `test_sprite_extractor` (requires OpenH264) — SpriteExtractor pipeline test
|
||||
- `test_bs_copy_bits` — Bulk bit copy correctness (aligned, unaligned, random round-trip)
|
||||
- `test_mux_perf` — Mux performance stress test: 1764 sprites, 160 frames, per-frame timing
|
||||
- `test_high_profile` (requires OpenH264) — High Profile SPS acceptance + large grid (52K MBs) mux verification
|
||||
- `test_ebsp_writer` — EbspWriter unit tests: flush_byte EBSP escaping, write_bits accumulation, exp-golomb LUT correctness, copy_blob aligned/unaligned/partial/sequence verification against bs_t+rbsp_to_ebsp reference, fast-path (5-arg copy_blob) correctness at all bit alignments with escape-free and boundary-escape scenarios
|
||||
- `test_row_plans` — Row plan precomputation correctness: single sprite, 2x2 grid, partial grid with inactive slots, empty grid (all double-wide slots)
|
||||
- `test_sprite_encode_alpha` (requires OpenH264) — SpriteEncoder double-wide canvas: alpha MB split, cbp_chroma=0 verification
|
||||
- `test_mux_alpha` (requires OpenH264) — End-to-end alpha mux: 2 sprites with varying alpha, pixel-identical verification of color and alpha regions against independent reference decode
|
||||
- `test_rbsp_writer` — RbspWriter + rbsp_to_ebsp_neon verification: compares two-pass (RbspWriter → rbsp_to_ebsp_neon) output against single-pass EbspWriter reference at all bit alignments, with escape-triggering patterns and random data
|
||||
- `test_ipcm` (requires OpenH264) — I_PCM IDR frame round-trip: write all-I_PCM IDR with gradient pattern, decode via OpenH264, pixel-identical verification
|
||||
- `test_resize` (requires OpenH264) — MuxSurface dynamic resize: compaction info query, grow/shrink resize, error handling (too few slots), frame counter preservation, pixel-identical verification of sprite content across resize
|
||||
|
||||
### Swift Tests (`swift test`)
|
||||
|
||||
**Note:** Generate YUV fixtures before running Swift tests: `cmake --build build --target fixtures`
|
||||
|
||||
- `testDecodedFrameCreation` — SCDecodedFrame data container
|
||||
- `testSpriteExtractor` — YUV fixture → SpriteExtractor → .mbs round-trip (160 frames)
|
||||
- `testSpriteEncoder` — YUV fixture → SpriteEncoder → H.264 stream (160 frames)
|
||||
- `testEncoderDecodeRoundTrip` — Encode → OpenH264 decode → verify dimensions (8 frames)
|
||||
- `testMuxSurfaceBasic` — MuxSurface create → add sprite → advance frame
|
||||
- `testStaggeredAddRemove` — **Main e2e test**: 3 sprites × 160 frames, staggered add/remove, pixel-identical verification against independent reference decode
|
||||
- `testVideoToolboxDecoder` — Encode 8 frames → VideoToolbox decode → verify frame count and dimensions
|
||||
- `testDecoderCrossComparison` — Encode 8 frames → decode with both OpenH264 and VideoToolbox → pixel-by-pixel YUV comparison with +/-1 tolerance
|
||||
- `testSpriteLooping` — 1 sprite × 320 frames (2 loops of 160), pixel-identical verification across loop boundary
|
||||
- `testAdvanceSpriteIndependent` — 2 sprites with independent frame rates via `advanceSpriteAtSlot:` + `emitFrameIfNeededWithSink:`, pixel-identical verification that only advanced sprite progresses
|
||||
- `testVideoToolboxPartialFill` — Partially-filled grid (10 sprites in 361-slot grid) decoded via VideoToolbox. Regression test for the write_skip_safe workaround — without it, VT rejects long skip_runs in partially-filled grids.
|
||||
- `testVideoToolboxIPCM` — MuxSurface resize with I_PCM transition frame decoded via VideoToolbox. Verifies VT handles I_PCM IDR + post-resize P-frames.
|
||||
- `testResizePerformance` — Resize performance: 420 sprites, resize 420→882 slots with real decoded pixels (OpenH264), wall-clock timing with performance gate (<200ms debug, ~5ms release).
|
||||
|
||||
## Sprite Multiplexing Pipeline
|
||||
|
||||
```
|
||||
Input video/YUV+alpha -> SpriteEncoder (double-wide padded canvas: color left, alpha right)
|
||||
-> H264Parser -> MacroblockData (split into color + alpha halves)
|
||||
-> save to .mbs (MbsSprite, pre-merged color+alpha row blobs per frame)
|
||||
-> MuxSurface (double-wide grid: color+alpha side-by-side per slot) -> composite H.264 stream
|
||||
-> hardware decoder (color and alpha regions decoded in a single frame)
|
||||
```
|
||||
|
||||
### How compositing works
|
||||
|
||||
1. **SpriteEncoder** (or **sprite_extract** CLI) encodes each sprite on a double-wide black-padded canvas: color left half (sprite content + padding border) and alpha right half (alpha-as-luma + neutral chroma Cb=Cr=128). For a 64x64 sprite with 16px padding, the canvas is 192x96 pixels (12x6 MBs). It parses the OpenH264 NAL output into `MacroblockData` via `H264Parser`, then splits into color (left half MBs) and alpha (right half MBs). Alpha MBs naturally have `cbp_chroma=0` (no chroma residual) since chroma is uniform 128.
|
||||
|
||||
2. **MBS encoding** (`mbs::encode_frame_merged`) serializes `MacroblockData` into the `.mbs` binary format (MBS v6, magic `MBS6`) with **real nC context** (not canonical nC=0) and **pre-merged color+alpha row blobs**. Color and alpha halves are encoded separately, then merged at encode time into a single blob per row: `[color_data][ue(inter_skip)][alpha_data]`, with leading/trailing skips relative to the full double-wide slot width. Alpha is always present — every sprite has both color and alpha data. The CAVLC is encoded with the actual neighbor-derived nC values, computed from the padded sprite canvas context. MBs are grouped into **row blobs**: each row's non-skip MBs are packed into a contiguous bitstream segment with interleaved skip_run exp-golomb codes. Each row's 6-byte header stores `leading_skips`, `trailing_skips`, packed `blob_bit_count` (15-bit count + 1-bit `has_long_zero_run` flag), `leading_zero_bits`, and `trailing_zero_bits`. The zero-run metadata is computed by scanning each merged blob at encode time. This means the CAVLC bitstream is already correct for the composite grid layout (see "Why real-nC works" below). No runtime merge is needed at load or mux time.
|
||||
|
||||
3. **MuxSurface** arranges sprites in a double-wide grid with shared padding borders. Each slot is `sprite_w * 2 - padding` MBs wide with color and alpha side-by-side, sharing a padding border between them. For the IDR frame (frame 0), all MBs are I_16x16 with DC prediction and zero residual (except MB(0,0) which needs a compensating luma DC coefficient since DC prediction defaults to 128 with no neighbors). This produces an all-black reference frame in ~1-2 bytes/MB. For P-frames, `advance_frame` has two phases: (a) `build_micro_ops` walks the precomputed `RowOp` plans and resolves all pointer chains into a flat `MicroOp` array (one entry per merged blob with data — inactive slots and all-skip rows are folded into skip counts); (b) `write_p_frame_micro` iterates the `MicroOp` array in a tight loop — `write_ue(skip)` + `copy_blob(merged_blob)` per op, using `EbspWriter` for inline EBSP escaping. When the blob's `has_long_zero_run` flag is false, `copy_blob` uses a fast path: only 3 boundary bytes go through `flush_byte` for EBSP escape checking, then the interior is bulk-copied (memcpy for aligned, NEON shift+write for non-aligned) with no EBSP checking — this is safe because the absence of 16-bit zero runs is alignment-invariant. No CAVLC re-encoding and no intermediate RBSP buffer needed. The composite grid layout is precomputed into flat `CompositeRowPlan` / `RowOp` arrays at `add_sprite`/`remove_sprite` time. `__builtin_prefetch` hides cache miss latency in both `build_micro_ops` and `write_p_frame_micro`.
|
||||
|
||||
4. New sprites are introduced mid-stream as I_16x16 MBs in P-frames (their frame 0 data), without requiring an IDR reset.
|
||||
|
||||
5. **Dynamic resize** (`MuxSurface::resize`) changes the grid size mid-stream. The caller provides decoded pixels of the last frame. MuxSurface emits new SPS/PPS + an all-I_PCM IDR with sprite pixels remapped to compacted slot positions in the new grid. Active sprites are compacted into slots 0..N-1 with frame counters preserved. Subsequent P-frames continue from the I_PCM reference. `check_compaction_opportunity` returns `CompactionInfo` (active/max slots, current/min grid MBs) so callers can decide when to resize. I_PCM costs 384 bytes/MB (up to 580 with EBSP escaping) — acceptable as a one-shot cost for an infrequent operation. The intermediate YUV planes use `make_unique_for_overwrite` + explicit `memset` (not `std::vector` fill) to avoid debug-mode per-element initialization overhead. The I_PCM IDR writer (`write_idr_ipcm`, inline in `mbs_mux_common.h`) uses single-pass EbspWriter with two paths: **black-MB fast path** (NEON detects all-black MBs Y=0/Cb=Cr=128, memcpy's precomputed EBSP pattern) and **non-black bulk path** (gathers MB samples into contiguous 384-byte buffer, single `flush_bytes` call with NEON-accelerated EBSP escaping). For a 420→882 slot resize (~45K MBs), this runs in ~2ms (Release) / ~16ms (Swift debug).
|
||||
|
||||
### Key design decisions and why
|
||||
|
||||
**Black-padded canvas (sprite + 1 MB border, hardcoded):**
|
||||
Padding is always exactly 1 MB (16px) — hardcoded throughout the pipeline, not configurable via any API. All user-facing APIs take content-only dimensions; padding is added internally. Motion vectors in P-frames may reference pixels outside the sprite content area. The black padding ensures these references produce identical pixels in both the independent encode and the composite. OpenH264's MV range is capped at 16px (1 MB) to stay within the padding.
|
||||
|
||||
**I_16x16 for IDR background:**
|
||||
The IDR frame uses all-I_16x16 with DC prediction to establish exact black pixels (Y=0, Cb=128, Cr=128). MB(0,0) has no neighbors so DC prediction defaults to 128 for luma; a compensating luma DC coefficient (`black_dc_level(qp)`) corrects this to Y=0. All other MBs predict 0 from already-decoded black neighbors → zero residual. This produces ~1-2 bytes/MB (vs ~385 bytes/MB with the previous I_PCM approach). Removed sprites become SKIP MBs referencing the black IDR — no active cleanup needed. Sprites loop indefinitely (frame counter wraps to 0 at `num_frames`), replaying their I_16x16 introduction frame at each loop boundary. Sprites are only removed via explicit `remove_sprite()`.
|
||||
|
||||
**Real-nC MBS encoding with row blobs (no CAVLC re-encoding at mux time):**
|
||||
The 1-MB padding border ensures that the nC context for every content block is identical between the original sprite canvas and the composite grid:
|
||||
- Content MBs neighbor either other content MBs from the same sprite (identical total_coeff) or SKIP padding (nC=0)
|
||||
- This is true in both the original encode and the composite layout
|
||||
- Therefore, CAVLC encoded with real nC on the sprite canvas is already correct for the composite
|
||||
|
||||
The same argument applies to MV prediction (SKIP padding has MV=0 in both contexts). Each row blob (exp-golomb skip runs + CAVLC blocks) can be copied verbatim at mux time via `EbspWriter::copy_blob`.
|
||||
|
||||
**Note:** Chroma DC blocks use nC=-1 (a fixed VLC table regardless of neighbors), so they never need re-encoding. Luma and chroma AC blocks use neighbor-derived nC, which is why real-nC encoding matters.
|
||||
|
||||
**Alpha channel (side-by-side in composite):**
|
||||
Alpha is required for all sprites — callers pass an alpha plane alongside Y/Cb/Cr (all-255 for opaque). SpriteEncoder encodes color+alpha on a single double-wide OpenH264 canvas (2× padded width × padded height). Alpha uses luma=alpha values with neutral chroma (Cb=Cr=128), producing `cbp_chroma=0` for all alpha MBs — no chroma data stored. In the composite, each slot places color and alpha side-by-side with shared padding: `[pad][color][shared pad][alpha][pad]` = `sprite_w * 2 - padding` MBs per slot. Color and alpha row blobs are pre-merged at encode time (MBS v6) into a single blob per row: `[color_data][ue(inter_skip)][alpha_data]`, with leading/trailing skips relative to the full slot width. The mux loop emits one merged blob per RowOp via a single `copy_blob` call. No runtime merge needed at load or mux time. Removed sprites' alpha regions become Y=0 (transparent) via SKIP referencing the all-black IDR.
|
||||
|
||||
**No intra MBs in P-frames (OpenH264 patched):**
|
||||
OpenH264 is patched (via `bSubcodecMode`) to prevent intra MB selection in P-slices and restrict all MBs to I_16x16/P_16x16/SKIP during encoding. This ensures P-frame MBs use only P_16x16 and SKIP (inter prediction from reference frame). I_16x16 MBs in P-frames are used only at the composite mux level for introducing new sprites — their I_16x16 data comes from the sprite's original IDR frame, not from OpenH264's P-frame encoding.
|
||||
|
||||
**OpenH264 for both encode and decode:**
|
||||
Using the same library for encoding (in sprite_extract) and decoding (in test verification) guarantees consistent behavior. Previously used x264+FFmpeg but hit QP mismatches and format inconsistencies.
|
||||
|
||||
**Automatic profile/level selection:**
|
||||
`write_headers` computes the H.264 level from the frame's MB count and selects Baseline Profile (up to Level 5.2 / 36,864 MBs) or High Profile (Level 6.0 / 139,264 MBs) automatically. High Profile uses CAVLC (not CABAC) and no 8x8 transforms — the bitstream content is identical to Baseline, just the SPS signals High Profile to unlock higher level limits. Required for 4K+ grids.
|
||||
|
||||
## Vendored OpenH264 Patches
|
||||
|
||||
> **Note:** These patches exist in Telegram's OpenH264 at `third-party/openh264/`. Subcodec does not carry its own OpenH264 copy — it uses the sibling directory via the `third_party/openh264_codec` symlink.
|
||||
|
||||
The OpenH264 used by subcodec has patches gated on `SEncParamExt::bSubcodecMode` (default false). When false, all encoder behavior is stock OpenH264. When true, sprite-compositing constraints activate. Set via `eparam.bSubcodecMode = true` before `InitializeExt()`. The flag is copied in `SWelsSvcCodingParam::ParamTranscode` (`encoder/core/inc/param_svc.h`).
|
||||
|
||||
**Conditional patches (bSubcodecMode=true):**
|
||||
|
||||
1. **No intra in P-frames** (`encoder/core/src/svc_base_layer_md.cpp`): `WelsMdFirstIntraMode()` returns false, preventing I_4x4/I_16x16 selection in P-slices.
|
||||
|
||||
2. **No I_4x4 in intra frames** (`encoder/core/src/svc_base_layer_md.cpp`): `WelsMdIntraFinePartition()` and `WelsMdIntraFinePartitionVaa()` skip I_4x4 evaluation — only I_16x16 allowed.
|
||||
|
||||
3. **No sub-partition modes** (`encoder/core/src/svc_base_layer_md.cpp`, `encoder/core/src/svc_mode_decision.cpp`): `WelsMdInterFinePartition()`, `WelsMdInterFinePartitionVaa()`, `WelsMdInterFinePartitionVaaOnScreen()` skip P_8x8/P_16x8/P_8x16 evaluation — only P_16x16/SKIP allowed.
|
||||
|
||||
4. **MV range cap** (`encoder/core/src/encoder_ext.cpp`): `GetMvMvdRange()` clamps `iMvRange` to 16 pixels max, matching the 1-MB padding border size.
|
||||
|
||||
5. **Padding reconstruction override** (`encoder/core/src/svc_encode_slice.cpp`): After encoding each MB, if it falls within the 1-MB border ring, reconstruction buffer is overwritten to exact black (Y=0, Cb=128, Cr=128). Hardcoded 1-MB padding.
|
||||
|
||||
6. **Log2MaxFrameNum = 4** (`encoder/core/src/au_set.cpp`): `WelsInitSps()` sets `uiLog2MaxFrameNum` to 4 (vs 15 stock) to match subcodec's H.264 parser. The bool is passed as a parameter through call sites in `paraset_strategy.cpp`.
|
||||
|
||||
**Unconditional patches (always active):**
|
||||
|
||||
7. **Level 6.0/6.1/6.2 support** (`api/wels/codec_app_def.h`, `common/inc/wels_common_defs.h`, `common/src/common_tables.cpp`, `decoder/core/src/au_parser.cpp`): Added H.264 Level 6.x entries (up to 139,264 MBs) to the level enum, limit table, level map, and decoder lookup. Stock OpenH264 maxes at Level 5.2 (36,864 MBs).
|
||||
|
||||
## Data Flow Details
|
||||
|
||||
### MacroblockData (types.h)
|
||||
|
||||
Stores all data needed to encode a macroblock:
|
||||
- `mb_type` — MbType::SKIP, P_16x16, I_16x16
|
||||
- `mv_x, mv_y` — Motion vector (half-pel, for P_16x16)
|
||||
- `intra_pred_mode, intra_chroma_mode` — Prediction modes (for I_16x16)
|
||||
- `luma_dc[16]` — I_16x16 DC coefficients
|
||||
- `luma_ac[16][15]` — AC coefficients per 4x4 block
|
||||
- `cb_dc[4], cr_dc[4], cb_ac[4][15], cr_ac[4][15]` — Chroma coefficients
|
||||
- `cbp_luma, cbp_chroma` — Coded block pattern
|
||||
|
||||
### MbsEncodedFrame (types.h)
|
||||
|
||||
Owned frame data returned by `mbs::encode_frame_merged()`:
|
||||
- `data` — `vector<uint8_t>` raw frame data (row metadata + merged blobs)
|
||||
- `rows` — `vector<MbsRow>` pre-merged row descriptors (color+alpha combined)
|
||||
|
||||
### MbsFrame (types.h)
|
||||
|
||||
View into bulk-owned frame data (used by MbsSprite after load or set_frames):
|
||||
- `merged_rows` — `span<MbsRow>` pre-merged color+alpha rows (slot_w-relative skips). Used by `build_micro_ops` for the P-frame mux path.
|
||||
|
||||
### MbsRow (types.h)
|
||||
|
||||
Per-row blob descriptor (6-byte on-disk header in MBS v6):
|
||||
- `leading_skips` — Content SKIPs before first non-skip MB in row
|
||||
- `trailing_skips` — Content SKIPs after last non-skip MB in row
|
||||
- `blob_bit_count` — Packed uint16_t: [14:0] = total bits in row blob, [15] = `has_long_zero_run` flag
|
||||
- `leading_zero_bits` — Zero bits at blob start (capped at 255)
|
||||
- `trailing_zero_bits` — Zero bits at blob end (capped at 255)
|
||||
- `blob_data` — Pointer into frame data buffer
|
||||
- `bit_count()` — Accessor returning lower 15 bits of `blob_bit_count`
|
||||
- `has_long_zero_run()` — Accessor returning top bit (true if any run of ≥16 consecutive zero bits)
|
||||
|
||||
### MbsSprite (types.h)
|
||||
|
||||
Binary MBS format sprite: per-frame `MbsFrame` views for streaming mux. Move-only. Every sprite has both color and alpha data (pre-merged in v6 format). Loaded from `.mbs` via `MbsSprite::load()` (single bulk read + view parse). Built from encoded frames via `set_frames()` which consolidates into bulk storage. Internally owns: `bulk_data_` (pre-merged blob bytes), `all_rows_` (merged MbsRow descriptors). All `MbsFrame` spans point into these.
|
||||
|
||||
Public fields: `width_mbs`, `height_mbs` (padded dimensions in MBs), `num_frames`, `qp`, `qp_delta_idr`, `qp_delta_p`, `frames` (vector of `MbsFrame` views). Padding is always 1 MB — not stored in the format or struct.
|
||||
|
||||
### FrameParams (types.h)
|
||||
|
||||
Frame dimensions and SPS parameters for the H.264 writers: `width_mbs`, `height_mbs`, `qp`, `log2_max_frame_num`, `pic_order_cnt_type`.
|
||||
|
||||
## Mux Performance
|
||||
|
||||
The mux path was heavily optimized. Key results at 1764 sprites (421x211 MB grid, double-wide with alpha):
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Sprite add (1764) | ~35 ms (0.02 ms/sprite; v6 pre-merged blobs, no runtime merge) |
|
||||
| Per-frame p50 | 0.18 ms |
|
||||
| Per-frame avg | 0.18 ms |
|
||||
|
||||
The hot path in `advance_frame` → `write_p_frame_micro` is:
|
||||
1. `build_micro_ops`: pre-resolve all blob pointers from `row_ops` + slot state into a flat `MicroOp` array (~33% of advance_frame time). Uses pre-merged rows (color+alpha combined at encode time in v6 format).
|
||||
2. `write_p_frame_micro`: tight loop over MicroOps — `write_ue(skip)` + `copy_blob(merged_blob)` per op, with EbspWriter's inline EBSP escaping (~66% of advance_frame time). With LTO, `copy_blob` is inlined.
|
||||
|
||||
Key optimizations applied:
|
||||
- **Pre-resolved MicroOps** (`build_micro_ops` walks `RowOp` plans once per frame and resolves all pointer chains `slot → sprite → frame → merged_rows → MbsRow` into a flat `MicroOp` array. The write loop iterates this array with zero pointer chasing, zero branching on inactive slots, and zero overlap conditionals)
|
||||
- **Pre-merged color/alpha row blobs** (at encode time, `encode_frame_merged` merges each sprite row's color and alpha blobs into a single blob: `[color_data][ue(inter_skip)][alpha_data]`. Stored pre-merged in MBS v6 format — no runtime merge at load or mux time. Halves the number of `copy_blob` calls per frame. Merged row metadata uses slot-relative leading/trailing skips)
|
||||
- **Single-pass EbspWriter** (`EbspWriter` writes directly to the output buffer with inline EBSP escape checking — no intermediate RBSP buffer, no separate `rbsp_to_ebsp` scan pass)
|
||||
- **Zero-run metadata fast path** (each row blob is scanned at encode time for runs of ≥16 consecutive zero bits. If none exist, `copy_blob` skips EBSP escape checking for interior bytes — only 3 boundary bytes go through `flush_byte`, then the interior is bulk-copied via memcpy (aligned) or NEON shift+write (non-aligned). This is safe because the absence of 16-bit zero runs is alignment-invariant: bit shifting doesn't create or destroy bits, so no alignment can produce two consecutive `0x00` bytes)
|
||||
- **NEON-accelerated non-aligned copy** (ARM NEON `vshlq_u8` processes 16 bytes per iteration for bit-shifted blob copies: two loads, two shifts, one OR, one store. Replaces scalar byte-at-a-time shift+write. Only used for escape-free blobs on the non-aligned path)
|
||||
- **Exp-golomb LUT** (pre-computed bit patterns for values 0-4095 — skip run writes are a single `write_bits` call instead of per-bit `bs_write_ue`)
|
||||
- **SWAR zero-byte detection** in `copy_blob` fallback path for blobs with long zero runs (8-byte chunks checked for zero bytes via `(v - 0x0101...) & ~v & 0x8080...`; safe regions memcpy'd directly)
|
||||
- **Bulk sprite loading** (`MbsSprite::load` reads entire file payload in one `fread`, parses view types into it — 3 heap allocs per sprite instead of ~3,200)
|
||||
- **Pre-built row blobs** eliminate CAVLC parsing at load time and enable row-level bulk copy at mux time
|
||||
- **Real-nC MBS encoding** (eliminates CAVLC re-encoding at mux time — row blob copy instead)
|
||||
- **Per-frame allocation reuse** (output buffer in MuxSurface, not per-frame heap allocs)
|
||||
- **Zero-free buffer allocation** (`buf_` uses `make_unique_for_overwrite` — no zeroing)
|
||||
- **All-I_16x16 IDR** (~1-2 bytes/MB vs ~385 bytes/MB with I_PCM) — negligible IDR overhead even for 4K+ grids
|
||||
- **Precomputed row plans** (grid layout is fixed between `add_sprite`/`remove_sprite`. `build_row_plans` precomputes a flat `RowOp` array with skip counts and overlap offsets. The mux loop iterates this plan with zero divisions, bounds checks, or slot_idx computation)
|
||||
- **Blob prefetch** (`__builtin_prefetch` on the next sprite's `blob_data` pointer hides L2/L3 cache miss latency. Both within-row and cross-row prefetch in both `build_micro_ops` and `write_p_frame_micro`)
|
||||
- **Lazy row plan rebuild** (row plans are only rebuilt on the first `advance_frame` after `add_sprite`/`remove_sprite`, not on every add/remove. Eliminates O(N²) plan rebuild cost when adding N sprites sequentially)
|
||||
|
||||
**Build note:** For best performance, build with `-DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON`. LTO enables cross-TU inlining of `copy_blob` into `write_p_frame_micro`, which is critical for performance (~40% improvement over non-LTO Release).
|
||||
|
||||
**Note:** `RbspWriter`, `rbsp_to_ebsp_neon`, and `write_p_frame_rbsp` (two-pass RBSP staging path) remain in the codebase for reference and testing. The active P-frame path is `write_p_frame_micro`. `bs_copy_bits` and `rbsp_to_ebsp` are used by the IDR path (`write_idr_black`), the encode-time merge in `encode_frame_merged`, and test reference comparisons. The resize I_PCM path (`write_idr_ipcm`) uses single-pass EbspWriter directly — no RBSP buffer or `rbsp_to_ebsp` needed.
|
||||
|
||||
## ObjC++ Wrapper (SubcodecObjC)
|
||||
|
||||
Test-quality ObjC++ bridge exposing the C++ API to Swift. `SC` prefix. One protocol and six classes:
|
||||
|
||||
- `SCSprite` — Two modes: **extractor** (`SCSprite.extractor(withSpriteSize:qp:outputPath:)` — raw YUV+alpha → .mbs file via SpriteExtractor) and **encoder** (`SCSprite.encoder(withWidth:height:qp:)` — raw content-size YUV+alpha → NAL data in memory via SpriteEncoder, for reference decode). Note: `finalizeExtraction()` not `finalize()` (avoids NSObject collision).
|
||||
- `SCMuxSurface` — Wraps MuxSurface. `SCMuxSurface.create(withSpriteWidth:spriteHeight:maxSlots:qp:sink:)` takes content pixels (no padding param). `addSpriteAtPath:error:` returns `SCSpriteRegion *` (slot, colorRect, alphaRect). `advanceSpriteAtSlot:` schedules one sprite for emit+advance. `emitFrameIfNeededWithSink:error:` emits a P-frame if any sprites are scheduled. `advanceFrameWithSink:error:` convenience that schedules all sprites then emits. `resizeToMaxSlots:yPlane:cbPlane:crPlane:...` returns `SCResizeResult *` (array of `SCSpriteRegion`). `checkCompactionOpportunity` returns `SCCompactionInfo *`.
|
||||
- `SCResizeResult` — Result of `resizeToMaxSlots:`: `regions` array of `SCSpriteRegion` with compacted slot assignments.
|
||||
- `SCCompactionInfo` — Result of `checkCompactionOpportunity`: `activeSprites`, `maxSlots`, `currentGridMbs`, `minGridMbs`.
|
||||
- `SCSpriteRegion` — Result of `addSpriteAtPath:error:`: `slot` index, `colorRect` and `alphaRect` as content pixel regions in composite frame.
|
||||
- `SCDecoding` — Protocol defining shared decoder interface: `createDecoderWithError:` and `decodeStream:error:`.
|
||||
- `SCOpenH264Decoder` — OpenH264 software decoder. Factory: `SCOpenH264Decoder.createDecoder()`. Conforms to `SCDecoding`.
|
||||
- `SCVideoToolboxDecoder` — VideoToolbox hardware decoder. Factory: `SCVideoToolboxDecoder.createDecoder()`. Conforms to `SCDecoding`. Converts Annex B → AVCC internally. Outputs NV12, deinterleaves to separate Y/Cb/Cr planes for `SCDecodedFrame`.
|
||||
- `SCDecodedFrame` — YUV plane data container (width, height, y, cb, cr as NSData).
|
||||
|
||||
## Production Readiness
|
||||
|
||||
### What's production-ready
|
||||
The core C++ library (`libsubcodec`) — .mbs format, real-nC encoding, MuxSurface compositing with merged-blob micro-op path — is proven correct with pixel-identical verification across 160 frames in both C++ and Swift tests. Stress-tested at 1764 sprites with ~0.18ms/frame p50 mux time (LTO Release). Verified with ffmpeg decode of real sprite data.
|
||||
|
||||
### What's needed for a production Apple app
|
||||
|
||||
1. **VideoToolbox integration (partially done)** — `SCVideoToolboxDecoder` provides bulk synchronous decode via `VTDecompressionSession`. For production, still needs:
|
||||
- Streaming frame-at-a-time decode timed to display cadence (CADisplayLink)
|
||||
- Display pipeline (CVPixelBuffer → Metal texture or CALayer)
|
||||
- The current wrapper copies YUV planes per frame; production should pass CVPixelBuffers directly to the display layer
|
||||
|
||||
2. **Streaming frame API** — Current ObjC++ wrapper writes into pre-allocated buffers. Production needs frame-at-a-time output timed to display cadence (CADisplayLink or similar).
|
||||
|
||||
3. **Encoding can be offline** — SpriteExtractor/.mbs generation can happen at build time or on a server. The app only needs MuxSurface (compositing) + VideoToolbox (decode). OpenH264 encoder need not ship in the app binary.
|
||||
|
||||
4. **Thread safety, error recovery, memory pressure** — Not addressed in current wrapper.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- **OpenH264 OOM at large resolutions:** OpenH264's decoder runs out of memory for frame sizes around 3K+ pixels per dimension. This only affects test verification — production uses VideoToolbox for decode.
|
||||
|
||||
- **I_PCM + I_16x16 mixing in IDR (OpenH264 bug):** OpenH264 returns `dsBitstreamError` when an IDR slice contains a mix of I_PCM and I_16x16 MBs due to `InitReadBits(pBs, 0)` corrupting bit-reader state after I_PCM parsing. The composite IDR uses all-I_16x16 (no mixing). The resize transition IDR uses all-I_PCM (no mixing). Both avoid the bug.
|
||||
|
||||
- **VideoToolbox rejects long skip_runs in partially-filled grids (worked around):** VT's H.264 decoder fails with `kVTVideoDecoderBadDataErr` (-8969) on structurally valid P-frames when large mb_skip_run values (from empty grid rows) interact with certain CAVLC blob patterns. The bitstream passes H264Parser and ffmpeg validation. Worked around by `write_skip_safe`: skip_runs exceeding `MAX_SKIP_RUN` (2048) are split by inserting dummy P_16x16 zero-residual MBs (4 bits each, semantically identical to SKIP). Tested by `testVideoToolboxPartialFill`.
|
||||
|
||||
## Profiling (advance_frame hot path)
|
||||
|
||||
### Tool: `bench_profile`
|
||||
|
||||
`tools/bench_profile.cpp` — parameterized profiling binary for `MuxSurface::advance_frame` and `MuxSurface::resize`. Uses `os_signpost` instrumentation, designed for `xctrace` CLI (non-interactive).
|
||||
|
||||
```bash
|
||||
# Generate demo .mbs (one-time, requires FFmpeg)
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
|
||||
./build/sprite_extract demo/input.mp4 demo/output0.mbs 64
|
||||
|
||||
# Build (MUST use Release for meaningful profiles)
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --target bench_profile
|
||||
|
||||
# Profile advance_frame with xctrace
|
||||
xctrace record --template 'Time Profiler' --output profile.trace \
|
||||
--launch -- ./build/bench_profile --mbs-path demo/output0.mbs \
|
||||
--sprite-count 1764 --frame-count 160 --loops 100
|
||||
|
||||
# Profile resize (420 active sprites, resize to 882 slots, 50 iterations)
|
||||
xctrace record --template 'Time Profiler' --output resize.trace \
|
||||
--launch -- ./build/bench_profile --mbs-path demo/output0.mbs \
|
||||
--profile-resize --resize-from 420 --resize-to 882 --resize-loops 50
|
||||
|
||||
# Export trace to XML for automated analysis
|
||||
xctrace export --input profile.trace \
|
||||
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]' > profile.xml
|
||||
```
|
||||
|
||||
CLI args: `--mbs-path <file>` or `--input <video> --sprite-size <N>`, `--sprite-count <N>` (default 1764), `--frame-count <N>` (default 160), `--loops <N>` (default 50, total frames = frame-count × loops), `--qp <N>` (default 26), `--profile-resize` (resize mode instead of advance_frame), `--resize-from <N>` (active sprites before resize, default 420), `--resize-to <N>` (target max_slots, default 882), `--resize-loops <N>` (iterations, default 10). The `--input` mode extracts .mbs from video via FFmpeg+SpriteExtractor (one-time setup, not profiled).
|
||||
|
||||
Sprites are pre-loaded into memory and added via `add_sprite(MbsSprite)` by move, so file I/O doesn't contaminate the profile. In advance_frame mode, only `advance_frame` is in the profiled section. In resize mode, each iteration creates a fresh MuxSurface with `resize-from` sprites, advances one frame, then times the resize to `resize-to` slots. Reports p50/avg/min/max across all iterations.
|
||||
|
||||
### Parsing xctrace XML export
|
||||
|
||||
The `time-profile` XML contains `<row>` elements with `<weight>`, `<tagged-backtrace>`, and `<sample-time>` children. Elements use `id`/`ref` deduplication — most rows reference the first occurrence via `ref="N"`. **Critical:** count samples (rows), don't sum weights. Weight refs often all point to the same 1ms value, making weight-based aggregation unreliable. Sample count gives the correct profile.
|
||||
|
||||
Parse pattern:
|
||||
1. Build `id_map` (all elements with `id` attr → element)
|
||||
2. For each `<row>`: resolve `<tagged-backtrace>` (may be ref), find `<backtrace>`, get `<frame>` list
|
||||
3. `frames[0]` = leaf (self time). `frames[0].get('name')` = function name
|
||||
4. Count samples per leaf function name
|
||||
|
||||
### Baseline profile (1764 sprites × 16K frames, demo/output0.mbs, LTO Release)
|
||||
|
||||
| Function | % CPU | Role |
|
||||
|---|---|---|
|
||||
| `write_p_frame_micro` (incl. inlined copy_blob) | 65% | Tight micro-op loop: write_ue + copy_blob per merged blob |
|
||||
| `build_micro_ops` | 33% | Pre-resolve blob pointers from row_ops + slot state |
|
||||
| `MuxSurface::advance_frame` | 1% | Frame dispatch overhead |
|
||||
| Other (memmove, memset) | 1% | |
|
||||
|
||||
Hot files: `src/mbs_mux_common.cpp` (write_p_frame_micro, build_micro_ops, EbspWriter::copy_blob), `src/mux_surface.cpp` (advance_frame dispatch).
|
||||
|
||||
Of the advance_frame-only time (~0.18ms p50), `write_p_frame_micro` is ~66% and `build_micro_ops` is ~33%. The `build_micro_ops` cost is dominated by pointer chasing through `slot → sprite → frames[idx] → merged_rows[row]` for ~8,862 ops per frame.
|
||||
|
||||
### Profile-optimize loop
|
||||
|
||||
1. **Profile:** `xctrace record` → export XML → parse sample counts per function
|
||||
2. **Analyze:** identify top self-time function, read its source
|
||||
3. **Optimize:** make ONE targeted change
|
||||
4. **Verify correctness:** `cd build && ctest` (all 25 tests must pass)
|
||||
5. **Re-profile:** same xctrace command, compare sample distribution
|
||||
6. **Commit or revert** based on results
|
||||
|
||||
### Important notes
|
||||
|
||||
- `test_mux_perf` is the existing wall-clock benchmark (1764 sprites, 160 frames, reports p50/p95/p99). Run it before and after optimizations for wall-clock comparison.
|
||||
- `test_ebsp_writer` specifically tests copy_blob correctness at all bit alignments — run after any copy_blob changes.
|
||||
- The demo .mbs file (`demo/output0.mbs`) must be generated first: `./build/sprite_extract demo/input.mp4 demo/output0.mbs 64`. It's a 64×64 sprite, 6×6 MBs padded, 160 frames.
|
||||
|
||||
## Conventions
|
||||
|
||||
- PascalCase for classes/structs (`MacroblockData`, `MbsSprite`, `MuxSurface`); snake_case for functions and variables; `UPPER_CASE` for enums and macros
|
||||
- Namespaces: `subcodec`, `subcodec::cavlc`, `subcodec::frame_writer`, `subcodec::mbs`, `subcodec::mux`, `subcodec::tables`
|
||||
- 4-space indentation
|
||||
- `std::expected<T, Error>` for fallible operations; `std::span<uint8_t>` for output buffers
|
||||
- RAII and `std::vector` for dynamic allocations; `std::unique_ptr` for pimpl
|
||||
- C++23 for all library and test code; OpenH264 (Telegram's copy) compiled as C++11
|
||||
- h264bitstream (vendored) remains C; `const_cast` used at `bs_t` interop boundaries
|
||||
181
third-party/subcodec/CMakeLists.txt
vendored
Normal file
181
third-party/subcodec/CMakeLists.txt
vendored
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(subcodec C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# OpenH264 root — sibling directory in telegram-ios/third-party/
|
||||
set(OH264_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../openh264/third_party/openh264/src/codec"
|
||||
CACHE PATH "Path to OpenH264 codec source directory")
|
||||
|
||||
if(NOT EXISTS "${OH264_ROOT}/api/wels/codec_api.h")
|
||||
message(FATAL_ERROR "OpenH264 not found at ${OH264_ROOT}. "
|
||||
"Expected sibling directory: ../openh264/third_party/openh264/src/codec/")
|
||||
endif()
|
||||
|
||||
# Find FFmpeg via pkg-config (required)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
|
||||
libavcodec
|
||||
libavformat
|
||||
libavutil
|
||||
libswscale
|
||||
)
|
||||
|
||||
# --- OpenH264 (from sibling directory) ---
|
||||
|
||||
# Common sources (shared by encoder and decoder)
|
||||
file(GLOB OH264_COMMON_SOURCES ${OH264_ROOT}/common/src/*.cpp)
|
||||
list(FILTER OH264_COMMON_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx)")
|
||||
|
||||
add_library(oh264_common STATIC ${OH264_COMMON_SOURCES})
|
||||
set_target_properties(oh264_common PROPERTIES CXX_STANDARD 11)
|
||||
target_compile_options(oh264_common PRIVATE -w)
|
||||
target_include_directories(oh264_common PUBLIC
|
||||
${OH264_ROOT}/api/wels
|
||||
${OH264_ROOT}/common/inc
|
||||
)
|
||||
|
||||
# Processing sources
|
||||
file(GLOB_RECURSE OH264_PROCESSING_SOURCES ${OH264_ROOT}/processing/src/*.cpp)
|
||||
list(FILTER OH264_PROCESSING_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx|loongarch)")
|
||||
|
||||
add_library(oh264_processing STATIC ${OH264_PROCESSING_SOURCES})
|
||||
set_target_properties(oh264_processing PROPERTIES CXX_STANDARD 11)
|
||||
target_compile_options(oh264_processing PRIVATE -w)
|
||||
target_link_libraries(oh264_processing PRIVATE oh264_common)
|
||||
target_include_directories(oh264_processing PRIVATE
|
||||
${OH264_ROOT}/processing/interface
|
||||
${OH264_ROOT}/processing/src/common
|
||||
)
|
||||
|
||||
# Encoder sources
|
||||
file(GLOB OH264_ENCODER_SOURCES ${OH264_ROOT}/encoder/core/src/*.cpp)
|
||||
list(FILTER OH264_ENCODER_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx)")
|
||||
list(APPEND OH264_ENCODER_SOURCES ${OH264_ROOT}/encoder/plus/src/welsEncoderExt.cpp)
|
||||
|
||||
add_library(oh264_encoder STATIC ${OH264_ENCODER_SOURCES})
|
||||
set_target_properties(oh264_encoder PROPERTIES CXX_STANDARD 11)
|
||||
target_compile_options(oh264_encoder PRIVATE -w)
|
||||
target_link_libraries(oh264_encoder PRIVATE oh264_common oh264_processing)
|
||||
target_include_directories(oh264_encoder PRIVATE
|
||||
${OH264_ROOT}/encoder/core/inc
|
||||
${OH264_ROOT}/encoder/plus/inc
|
||||
${OH264_ROOT}/processing/interface
|
||||
)
|
||||
|
||||
# Decoder sources
|
||||
file(GLOB OH264_DECODER_SOURCES
|
||||
${OH264_ROOT}/decoder/core/src/*.cpp
|
||||
${OH264_ROOT}/decoder/plus/src/*.cpp
|
||||
)
|
||||
list(FILTER OH264_DECODER_SOURCES EXCLUDE REGEX "(_neon|_mmi|_msa|_lsx|_lasx|DllEntry)")
|
||||
|
||||
add_library(oh264_decoder STATIC ${OH264_DECODER_SOURCES})
|
||||
set_target_properties(oh264_decoder PROPERTIES CXX_STANDARD 11)
|
||||
target_compile_options(oh264_decoder PRIVATE -w)
|
||||
target_link_libraries(oh264_decoder PRIVATE oh264_common)
|
||||
target_include_directories(oh264_decoder PRIVATE
|
||||
${OH264_ROOT}/decoder/core/inc
|
||||
${OH264_ROOT}/decoder/plus/inc
|
||||
)
|
||||
|
||||
# Umbrella interface library
|
||||
add_library(openh264 INTERFACE)
|
||||
target_link_libraries(openh264 INTERFACE oh264_encoder oh264_decoder oh264_common oh264_processing)
|
||||
target_include_directories(openh264 INTERFACE ${OH264_ROOT}/api/wels)
|
||||
|
||||
# --- Vendored h264bitstream ---
|
||||
|
||||
add_library(h264bitstream STATIC
|
||||
third_party/h264bitstream/h264_stream.c
|
||||
third_party/h264bitstream/h264_nal.c
|
||||
third_party/h264bitstream/h264_sei.c
|
||||
)
|
||||
target_include_directories(h264bitstream PUBLIC third_party/h264bitstream)
|
||||
|
||||
# --- Core subcodec library ---
|
||||
|
||||
add_library(subcodec STATIC
|
||||
src/frame_writer.cpp
|
||||
src/cavlc.cpp
|
||||
src/h264_parser.cpp
|
||||
src/sprite_data.cpp
|
||||
src/mbs_encode.cpp
|
||||
src/mbs_mux_common.cpp
|
||||
src/mux_surface.cpp
|
||||
)
|
||||
target_include_directories(subcodec PUBLIC src)
|
||||
target_link_libraries(subcodec PUBLIC h264bitstream)
|
||||
|
||||
# --- Sprite encoder library (OpenH264 wrapper) ---
|
||||
|
||||
add_library(sprite_encode STATIC src/sprite_encode.cpp src/sprite_extractor.cpp)
|
||||
target_compile_options(sprite_encode PRIVATE -w)
|
||||
target_link_libraries(sprite_encode PUBLIC subcodec openh264)
|
||||
target_include_directories(sprite_encode PUBLIC src)
|
||||
|
||||
# --- Tools (require FFmpeg) ---
|
||||
|
||||
add_executable(sprite_extract tools/sprite_extract.cpp)
|
||||
target_link_libraries(sprite_extract sprite_encode PkgConfig::LIBAV)
|
||||
|
||||
add_executable(sprite_mux tools/sprite_mux.cpp)
|
||||
target_link_libraries(sprite_mux subcodec)
|
||||
target_include_directories(sprite_mux PRIVATE src)
|
||||
|
||||
add_executable(bench_profile tools/bench_profile.cpp)
|
||||
target_link_libraries(bench_profile sprite_encode PkgConfig::LIBAV)
|
||||
target_compile_options(bench_profile PRIVATE -w)
|
||||
|
||||
# --- Fixture generation ---
|
||||
|
||||
add_executable(generate_fixtures Tests/SubcodecTests/generate_fixtures.cpp)
|
||||
|
||||
set(FIXTURE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Tests/SubcodecTests/Fixtures)
|
||||
|
||||
add_custom_target(fixtures
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${FIXTURE_DIR}
|
||||
COMMAND $<TARGET_FILE:generate_fixtures> ${FIXTURE_DIR}
|
||||
DEPENDS generate_fixtures
|
||||
COMMENT "Generating YUV test fixtures in Tests/SubcodecTests/Fixtures/"
|
||||
)
|
||||
|
||||
# --- Tests ---
|
||||
|
||||
# Core tests (link subcodec only)
|
||||
set(CORE_TESTS
|
||||
test_cavlc test_cavlc_read test_cavlc_split test_ct_lut
|
||||
test_mb_p16x16 test_mb_i16x16
|
||||
test_p_frame_ex test_idr_frame_ex test_h264_parse
|
||||
test_mbs_format test_mbs_encode
|
||||
test_bs_copy_bits test_ebsp_writer test_rbsp_writer
|
||||
test_row_plans test_mux_perf
|
||||
)
|
||||
|
||||
foreach(t ${CORE_TESTS})
|
||||
add_executable(${t} test/${t}.cpp)
|
||||
target_link_libraries(${t} subcodec)
|
||||
target_include_directories(${t} PRIVATE src third_party/h264bitstream)
|
||||
endforeach()
|
||||
|
||||
# E2E tests (link sprite_encode = subcodec + openh264)
|
||||
set(E2E_TESTS
|
||||
test_mux test_mux_surface test_mux_alpha
|
||||
test_cavlc_diag test_sprite_extractor test_sprite_encode_alpha
|
||||
test_high_profile test_ipcm test_resize
|
||||
)
|
||||
|
||||
foreach(t ${E2E_TESTS})
|
||||
add_executable(${t} test/${t}.cpp)
|
||||
target_link_libraries(${t} sprite_encode)
|
||||
endforeach()
|
||||
|
||||
# --- CTest registration ---
|
||||
|
||||
enable_testing()
|
||||
|
||||
foreach(t ${CORE_TESTS} ${E2E_TESTS})
|
||||
add_test(NAME ${t} COMMAND ${t})
|
||||
endforeach()
|
||||
142
third-party/subcodec/Package.swift
vendored
Normal file
142
third-party/subcodec/Package.swift
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
let oh264 = "third_party/openh264_codec"
|
||||
|
||||
let package = Package(
|
||||
name: "subcodec",
|
||||
platforms: [
|
||||
.macOS(.v10_15),
|
||||
.iOS(.v13),
|
||||
.tvOS(.v13),
|
||||
],
|
||||
products: [
|
||||
.library(name: "SubcodecObjC", targets: ["SubcodecObjC"]),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "oh264_common",
|
||||
path: "\(oh264)/common",
|
||||
exclude: [
|
||||
"arm", "arm64", "x86", "mips", "loongarch",
|
||||
],
|
||||
sources: ["src"],
|
||||
publicHeadersPath: "inc",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("../api/wels"),
|
||||
.unsafeFlags(["-w", "-std=c++11"]),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "oh264_processing",
|
||||
dependencies: ["oh264_common"],
|
||||
path: "\(oh264)/processing",
|
||||
exclude: [
|
||||
"src/arm", "src/arm64", "src/x86", "src/mips", "src/loongarch",
|
||||
"src/common/WelsVP.rc", "src/common/WelsVP.def",
|
||||
],
|
||||
sources: ["src"],
|
||||
publicHeadersPath: "interface",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("src/common"),
|
||||
.headerSearchPath("../common/inc"),
|
||||
.headerSearchPath("../api/wels"),
|
||||
.unsafeFlags(["-w", "-std=c++11"]),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "oh264_encoder",
|
||||
dependencies: ["oh264_common", "oh264_processing"],
|
||||
path: "\(oh264)/encoder",
|
||||
exclude: [
|
||||
"core/arm", "core/arm64", "core/x86", "core/mips", "core/loongarch",
|
||||
"plus/src/DllEntry.cpp", "plus/src/wels_enc_export.def",
|
||||
],
|
||||
sources: ["core/src", "plus/src"],
|
||||
publicHeadersPath: "core/inc",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("plus/inc"),
|
||||
.headerSearchPath("../common/inc"),
|
||||
.headerSearchPath("../api/wels"),
|
||||
.headerSearchPath("../processing/interface"),
|
||||
.unsafeFlags(["-w", "-std=c++11"]),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "oh264_decoder",
|
||||
dependencies: ["oh264_common"],
|
||||
path: "\(oh264)/decoder",
|
||||
exclude: [
|
||||
"core/arm", "core/arm64", "core/x86", "core/mips", "core/loongarch",
|
||||
"plus/src/wels_dec_export.def",
|
||||
],
|
||||
sources: ["core/src", "plus/src"],
|
||||
publicHeadersPath: "core/inc",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("plus/inc"),
|
||||
.headerSearchPath("../common/inc"),
|
||||
.headerSearchPath("../api/wels"),
|
||||
.unsafeFlags(["-w", "-std=c++11"]),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "h264bitstream",
|
||||
path: "third_party/h264bitstream",
|
||||
sources: ["h264_stream.c", "h264_nal.c", "h264_sei.c"],
|
||||
publicHeadersPath: "."
|
||||
),
|
||||
.target(
|
||||
name: "sprite_encode",
|
||||
dependencies: ["subcodec", "oh264_common", "oh264_processing", "oh264_encoder", "oh264_decoder"],
|
||||
path: "Sources/SpriteEncode",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("../../src"),
|
||||
.headerSearchPath("../../third_party/h264bitstream"),
|
||||
.headerSearchPath("../../\(oh264)/api/wels"),
|
||||
.headerSearchPath("../../\(oh264)/encoder/core/inc"),
|
||||
.headerSearchPath("../../\(oh264)/encoder/plus/inc"),
|
||||
.headerSearchPath("../../\(oh264)/decoder/core/inc"),
|
||||
.headerSearchPath("../../\(oh264)/decoder/plus/inc"),
|
||||
.headerSearchPath("../../\(oh264)/common/inc"),
|
||||
.unsafeFlags(["-std=c++23"]),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "subcodec",
|
||||
dependencies: ["h264bitstream"],
|
||||
path: "src",
|
||||
exclude: ["sprite_encode.cpp", "sprite_extractor.cpp"],
|
||||
publicHeadersPath: ".",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("../third_party/h264bitstream"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "SubcodecObjC",
|
||||
dependencies: ["subcodec", "sprite_encode"],
|
||||
path: "Sources/SubcodecObjC",
|
||||
publicHeadersPath: "include",
|
||||
cxxSettings: [
|
||||
.headerSearchPath("../../src"),
|
||||
.headerSearchPath("../../third_party/h264bitstream"),
|
||||
.headerSearchPath("../../\(oh264)/api/wels"),
|
||||
.headerSearchPath("../../\(oh264)/common/inc"),
|
||||
.headerSearchPath("../../\(oh264)/encoder/core/inc"),
|
||||
.headerSearchPath("../../\(oh264)/decoder/core/inc"),
|
||||
],
|
||||
linkerSettings: [
|
||||
.linkedFramework("VideoToolbox"),
|
||||
.linkedFramework("CoreMedia"),
|
||||
.linkedFramework("CoreVideo"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "SubcodecTests",
|
||||
dependencies: ["SubcodecObjC"],
|
||||
path: "Tests/SubcodecTests",
|
||||
exclude: ["generate_fixtures.cpp"],
|
||||
resources: [.copy("Fixtures")]
|
||||
),
|
||||
],
|
||||
cxxLanguageStandard: .cxx2b
|
||||
)
|
||||
1
third-party/subcodec/Sources/SpriteEncode/include/sprite_encode_target.h
vendored
Normal file
1
third-party/subcodec/Sources/SpriteEncode/include/sprite_encode_target.h
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Placeholder header for SwiftPM target — actual headers are in src/
|
||||
1
third-party/subcodec/Sources/SpriteEncode/sprite_encode.cpp
vendored
Normal file
1
third-party/subcodec/Sources/SpriteEncode/sprite_encode.cpp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
#include "../../src/sprite_encode.cpp"
|
||||
1
third-party/subcodec/Sources/SpriteEncode/sprite_extractor.cpp
vendored
Normal file
1
third-party/subcodec/Sources/SpriteEncode/sprite_extractor.cpp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
#include "../../src/sprite_extractor.cpp"
|
||||
46
third-party/subcodec/Sources/SubcodecObjC/AnnexBSplitter.h
vendored
Normal file
46
third-party/subcodec/Sources/SubcodecObjC/AnnexBSplitter.h
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Sources/SubcodecObjC/AnnexBSplitter.h
|
||||
// Internal shared header — not in public include/ directory
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
struct AnnexBFrame {
|
||||
const uint8_t *data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
inline std::vector<AnnexBFrame> split_annex_b_frames(const uint8_t* data, size_t size) {
|
||||
std::vector<AnnexBFrame> frames;
|
||||
size_t frame_start = 0;
|
||||
bool current_has_slice = false;
|
||||
|
||||
for (size_t i = 0; i + 3 < size; ) {
|
||||
int sc_len = 0;
|
||||
if (i + 3 < size && data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1)
|
||||
sc_len = 4;
|
||||
else if (i + 2 < size && data[i] == 0 && data[i+1] == 0 && data[i+2] == 1)
|
||||
sc_len = 3;
|
||||
|
||||
if (sc_len > 0 && i > 0) {
|
||||
uint8_t nal_type = data[i + sc_len] & 0x1F;
|
||||
if ((nal_type == 1 || nal_type == 5) && i > frame_start) {
|
||||
if (current_has_slice) {
|
||||
frames.push_back({data + frame_start, i - frame_start});
|
||||
frame_start = i;
|
||||
current_has_slice = false;
|
||||
}
|
||||
current_has_slice = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sc_len > 0) i += sc_len + 1;
|
||||
else i++;
|
||||
}
|
||||
|
||||
if (frame_start < size) {
|
||||
frames.push_back({data + frame_start, size - frame_start});
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
22
third-party/subcodec/Sources/SubcodecObjC/SCDecodedFrame.mm
vendored
Normal file
22
third-party/subcodec/Sources/SubcodecObjC/SCDecodedFrame.mm
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Sources/SubcodecObjC/SCDecodedFrame.mm
|
||||
#import "SCDecodedFrame.h"
|
||||
|
||||
@implementation SCDecodedFrame
|
||||
|
||||
- (instancetype)initWithWidth:(int)width
|
||||
height:(int)height
|
||||
y:(NSData *)y
|
||||
cb:(NSData *)cb
|
||||
cr:(NSData *)cr {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_width = width;
|
||||
_height = height;
|
||||
_y = [y copy];
|
||||
_cb = [cb copy];
|
||||
_cr = [cr copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
227
third-party/subcodec/Sources/SubcodecObjC/SCMuxSurface.mm
vendored
Normal file
227
third-party/subcodec/Sources/SubcodecObjC/SCMuxSurface.mm
vendored
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// Sources/SubcodecObjC/SCMuxSurface.mm
|
||||
#import "SCMuxSurface.h"
|
||||
|
||||
#include "mux_surface.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
using namespace subcodec;
|
||||
|
||||
static NSError* makeError(NSString* msg) {
|
||||
return [NSError errorWithDomain:@"SCMuxSurface" code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||
}
|
||||
|
||||
@implementation SCCompactionInfo
|
||||
|
||||
- (instancetype)initWithActiveSprites:(int)active
|
||||
maxSlots:(int)max
|
||||
currentGridMbs:(int)current
|
||||
minGridMbs:(int)min {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_activeSprites = active;
|
||||
_maxSlots = max;
|
||||
_currentGridMbs = current;
|
||||
_minGridMbs = min;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SCResizeResult
|
||||
|
||||
- (instancetype)initWithRegions:(NSArray<SCSpriteRegion *> *)regions {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_regions = [regions copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SCMuxSurface {
|
||||
std::optional<MuxSurface> _surface;
|
||||
}
|
||||
|
||||
+ (nullable SCMuxSurface *)createWithSpriteWidth:(int)width
|
||||
spriteHeight:(int)height
|
||||
maxSlots:(int)slots
|
||||
qp:(int)qp
|
||||
sink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error {
|
||||
MuxSurface::Params params;
|
||||
params.sprite_width = width;
|
||||
params.sprite_height = height;
|
||||
params.max_slots = slots;
|
||||
params.qp = qp;
|
||||
params.qp_delta_idr = 0;
|
||||
params.qp_delta_p = 0;
|
||||
|
||||
auto result = MuxSurface::create(params, [sink](std::span<const uint8_t> data) {
|
||||
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
|
||||
length:data.size()
|
||||
freeWhenDone:NO];
|
||||
sink(nsData);
|
||||
});
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"MuxSurface::create failed");
|
||||
return nil;
|
||||
}
|
||||
|
||||
SCMuxSurface* obj = [[SCMuxSurface alloc] init];
|
||||
obj->_surface.emplace(std::move(*result));
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (int)widthMbs {
|
||||
return _surface ? _surface->width_mbs() : 0;
|
||||
}
|
||||
|
||||
- (int)heightMbs {
|
||||
return _surface ? _surface->height_mbs() : 0;
|
||||
}
|
||||
|
||||
- (nullable SCSpriteRegion *)addSpriteAtPath:(NSString *)path
|
||||
error:(NSError **)error {
|
||||
if (!_surface) {
|
||||
if (error) *error = makeError(@"Surface not initialized");
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto result = _surface->add_sprite(path.UTF8String);
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"add_sprite failed");
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto& region = *result;
|
||||
CGRect colorRect = CGRectMake(region.color.x, region.color.y,
|
||||
region.color.width, region.color.height);
|
||||
CGRect alphaRect = CGRectMake(region.alpha.x, region.alpha.y,
|
||||
region.alpha.width, region.alpha.height);
|
||||
return [[SCSpriteRegion alloc] initWithSlot:region.slot
|
||||
colorRect:colorRect
|
||||
alphaRect:alphaRect];
|
||||
}
|
||||
|
||||
- (void)removeSpriteAtSlot:(int)slot {
|
||||
if (_surface) {
|
||||
_surface->remove_sprite(slot);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)advanceSpriteAtSlot:(int)slot {
|
||||
if (_surface) {
|
||||
_surface->advance_sprite(slot);
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)emitFrameIfNeededWithSink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error {
|
||||
if (!_surface) {
|
||||
if (error) *error = makeError(@"Surface not initialized");
|
||||
return NO;
|
||||
}
|
||||
|
||||
auto result = _surface->emit_frame_if_needed([sink](std::span<const uint8_t> data) {
|
||||
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
|
||||
length:data.size()
|
||||
freeWhenDone:NO];
|
||||
sink(nsData);
|
||||
});
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"emit_frame_if_needed failed");
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)advanceFrameWithSink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error {
|
||||
if (!_surface) {
|
||||
if (error) *error = makeError(@"Surface not initialized");
|
||||
return NO;
|
||||
}
|
||||
|
||||
auto result = _surface->advance_frame([sink](std::span<const uint8_t> data) {
|
||||
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
|
||||
length:data.size()
|
||||
freeWhenDone:NO];
|
||||
sink(nsData);
|
||||
});
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"advance_frame failed");
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable SCResizeResult *)resizeToMaxSlots:(int)newMaxSlots
|
||||
yPlane:(NSData *)yPlane
|
||||
cbPlane:(NSData *)cbPlane
|
||||
crPlane:(NSData *)crPlane
|
||||
decodedWidth:(int)decodedWidth
|
||||
decodedHeight:(int)decodedHeight
|
||||
strideY:(int)strideY
|
||||
strideCb:(int)strideCb
|
||||
strideCr:(int)strideCr
|
||||
withSink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error {
|
||||
if (!_surface) {
|
||||
if (error) *error = makeError(@"Surface not initialized");
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto result = _surface->resize(
|
||||
newMaxSlots,
|
||||
{(const uint8_t*)yPlane.bytes, yPlane.length},
|
||||
{(const uint8_t*)cbPlane.bytes, cbPlane.length},
|
||||
{(const uint8_t*)crPlane.bytes, crPlane.length},
|
||||
decodedWidth, decodedHeight,
|
||||
strideY, strideCb, strideCr,
|
||||
[sink](std::span<const uint8_t> data) {
|
||||
NSData* nsData = [NSData dataWithBytesNoCopy:(void*)data.data()
|
||||
length:data.size()
|
||||
freeWhenDone:NO];
|
||||
sink(nsData);
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"resize failed");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<SCSpriteRegion *> *regions = [NSMutableArray array];
|
||||
|
||||
for (auto& region : result->regions) {
|
||||
CGRect colorRect = CGRectMake(region.color.x, region.color.y,
|
||||
region.color.width, region.color.height);
|
||||
CGRect alphaRect = CGRectMake(region.alpha.x, region.alpha.y,
|
||||
region.alpha.width, region.alpha.height);
|
||||
[regions addObject:[[SCSpriteRegion alloc] initWithSlot:region.slot
|
||||
colorRect:colorRect
|
||||
alphaRect:alphaRect]];
|
||||
}
|
||||
|
||||
return [[SCResizeResult alloc] initWithRegions:regions];
|
||||
}
|
||||
|
||||
- (SCCompactionInfo *)checkCompactionOpportunity {
|
||||
if (!_surface) {
|
||||
return [[SCCompactionInfo alloc] initWithActiveSprites:0
|
||||
maxSlots:0
|
||||
currentGridMbs:0
|
||||
minGridMbs:0];
|
||||
}
|
||||
|
||||
auto info = _surface->check_compaction_opportunity();
|
||||
return [[SCCompactionInfo alloc] initWithActiveSprites:info.active_sprites
|
||||
maxSlots:info.max_slots
|
||||
currentGridMbs:info.current_grid_mbs
|
||||
minGridMbs:info.min_grid_mbs];
|
||||
}
|
||||
|
||||
@end
|
||||
98
third-party/subcodec/Sources/SubcodecObjC/SCOpenH264Decoder.mm
vendored
Normal file
98
third-party/subcodec/Sources/SubcodecObjC/SCOpenH264Decoder.mm
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Sources/SubcodecObjC/SCOpenH264Decoder.mm
|
||||
#import "SCOpenH264Decoder.h"
|
||||
|
||||
#include "codec_api.h"
|
||||
#include "codec_app_def.h"
|
||||
#include "codec_def.h"
|
||||
|
||||
#include "AnnexBSplitter.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
static NSError* makeError(NSString* msg) {
|
||||
return [NSError errorWithDomain:@"SCOpenH264Decoder" code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||
}
|
||||
|
||||
@implementation SCOpenH264Decoder {
|
||||
ISVCDecoder* _decoder;
|
||||
}
|
||||
|
||||
+ (nullable SCOpenH264Decoder *)createDecoderWithError:(NSError **)error {
|
||||
SCOpenH264Decoder* obj = [[SCOpenH264Decoder alloc] initInternal];
|
||||
if (!obj) {
|
||||
if (error) *error = makeError(@"Failed to create decoder");
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initInternal {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
if (WelsCreateDecoder(&_decoder) != 0 || !_decoder) return nil;
|
||||
|
||||
SDecodingParam decParam;
|
||||
memset(&decParam, 0, sizeof(decParam));
|
||||
decParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC;
|
||||
if (_decoder->Initialize(&decParam) != 0) {
|
||||
WelsDestroyDecoder(_decoder);
|
||||
_decoder = nullptr;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_decoder) {
|
||||
WelsDestroyDecoder(_decoder);
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
|
||||
error:(NSError **)error {
|
||||
auto packets = split_annex_b_frames((const uint8_t*)data.bytes, data.length);
|
||||
|
||||
NSMutableArray<SCDecodedFrame *>* frames = [NSMutableArray array];
|
||||
|
||||
for (auto& pkt : packets) {
|
||||
unsigned char* pDst[3] = {nullptr};
|
||||
SBufferInfo dstInfo;
|
||||
memset(&dstInfo, 0, sizeof(dstInfo));
|
||||
|
||||
_decoder->DecodeFrameNoDelay(const_cast<unsigned char*>(pkt.data),
|
||||
(int)pkt.size, pDst, &dstInfo);
|
||||
|
||||
if (dstInfo.iBufferStatus == 1) {
|
||||
int w = dstInfo.UsrData.sSystemBuffer.iWidth;
|
||||
int h = dstInfo.UsrData.sSystemBuffer.iHeight;
|
||||
int stride_y = dstInfo.UsrData.sSystemBuffer.iStride[0];
|
||||
int stride_uv = dstInfo.UsrData.sSystemBuffer.iStride[1];
|
||||
|
||||
NSMutableData* yData = [NSMutableData dataWithLength:w * h];
|
||||
NSMutableData* cbData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
|
||||
NSMutableData* crData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
|
||||
|
||||
uint8_t* yDst = (uint8_t*)yData.mutableBytes;
|
||||
uint8_t* cbDst = (uint8_t*)cbData.mutableBytes;
|
||||
uint8_t* crDst = (uint8_t*)crData.mutableBytes;
|
||||
|
||||
for (int r = 0; r < h; r++)
|
||||
memcpy(yDst + r * w, pDst[0] + r * stride_y, w);
|
||||
for (int r = 0; r < h / 2; r++) {
|
||||
memcpy(cbDst + r * (w / 2), pDst[1] + r * stride_uv, w / 2);
|
||||
memcpy(crDst + r * (w / 2), pDst[2] + r * stride_uv, w / 2);
|
||||
}
|
||||
|
||||
SCDecodedFrame* frame = [[SCDecodedFrame alloc] initWithWidth:w height:h
|
||||
y:yData cb:cbData cr:crData];
|
||||
[frames addObject:frame];
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
@end
|
||||
172
third-party/subcodec/Sources/SubcodecObjC/SCSprite.mm
vendored
Normal file
172
third-party/subcodec/Sources/SubcodecObjC/SCSprite.mm
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// Sources/SubcodecObjC/SCSprite.mm
|
||||
#import "SCSprite.h"
|
||||
|
||||
#include "sprite_extractor.h"
|
||||
#include "sprite_encode.h"
|
||||
#include "frame_writer.h"
|
||||
#include "types.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
using namespace subcodec;
|
||||
|
||||
static NSError* makeError(NSString* msg) {
|
||||
return [NSError errorWithDomain:@"SCSprite" code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||
}
|
||||
|
||||
@implementation SCSprite {
|
||||
// Extractor mode
|
||||
std::optional<SpriteExtractor> _extractor;
|
||||
|
||||
// Encoder mode
|
||||
std::optional<SpriteEncoder> _encoder;
|
||||
std::vector<std::vector<uint8_t>> _nalFrames;
|
||||
int _paddedWidth;
|
||||
int _paddedHeight;
|
||||
int _qp;
|
||||
}
|
||||
|
||||
+ (nullable SCSprite *)extractorWithSpriteSize:(int)size
|
||||
qp:(int)qp
|
||||
outputPath:(NSString *)path
|
||||
error:(NSError **)error {
|
||||
SpriteExtractor::Params params;
|
||||
params.sprite_size = size;
|
||||
params.qp = qp;
|
||||
|
||||
auto result = SpriteExtractor::create(params, path.UTF8String);
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"Failed to create SpriteExtractor");
|
||||
return nil;
|
||||
}
|
||||
|
||||
SCSprite* sprite = [[SCSprite alloc] init];
|
||||
sprite->_extractor.emplace(std::move(*result));
|
||||
return sprite;
|
||||
}
|
||||
|
||||
- (BOOL)addFrameY:(NSData *)y yStride:(int)ys
|
||||
cb:(NSData *)cb cbStride:(int)cbs
|
||||
cr:(NSData *)cr crStride:(int)crs
|
||||
alpha:(NSData *)alpha alphaStride:(int)as
|
||||
error:(NSError **)error {
|
||||
if (!_extractor) {
|
||||
if (error) *error = makeError(@"Not in extractor mode");
|
||||
return NO;
|
||||
}
|
||||
|
||||
auto result = _extractor->add_frame(
|
||||
(const uint8_t*)y.bytes, ys,
|
||||
(const uint8_t*)cb.bytes, cbs,
|
||||
(const uint8_t*)cr.bytes, crs,
|
||||
(const uint8_t*)alpha.bytes, as);
|
||||
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"add_frame failed");
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)finalizeExtraction:(NSError **)error {
|
||||
if (!_extractor) {
|
||||
if (error) *error = makeError(@"Not in extractor mode");
|
||||
return NO;
|
||||
}
|
||||
auto result = _extractor->finalize();
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"finalize failed");
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Encoder path
|
||||
+ (nullable SCSprite *)encoderWithWidth:(int)width
|
||||
height:(int)height
|
||||
qp:(int)qp
|
||||
error:(NSError **)error {
|
||||
SpriteEncoder::Params params;
|
||||
params.width = width;
|
||||
params.height = height;
|
||||
params.qp = qp;
|
||||
|
||||
auto result = SpriteEncoder::create(params);
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"Failed to create SpriteEncoder");
|
||||
return nil;
|
||||
}
|
||||
|
||||
SCSprite* sprite = [[SCSprite alloc] init];
|
||||
sprite->_encoder.emplace(std::move(*result));
|
||||
sprite->_paddedWidth = width + 2 * 16;
|
||||
sprite->_paddedHeight = height + 2 * 16;
|
||||
sprite->_qp = qp;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
- (BOOL)encodeFrameY:(NSData *)y yStride:(int)ys
|
||||
cb:(NSData *)cb cbStride:(int)cbs
|
||||
cr:(NSData *)cr crStride:(int)crs
|
||||
frameIndex:(int)idx
|
||||
error:(NSError **)error {
|
||||
if (!_encoder) {
|
||||
if (error) *error = makeError(@"Not in encoder mode");
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Create opaque alpha buffer for encoder (no alpha source in ObjC wrapper yet)
|
||||
std::vector<uint8_t> alpha_buf(_paddedWidth * _paddedHeight, 255);
|
||||
|
||||
std::vector<uint8_t> nal;
|
||||
auto result = _encoder->encode(
|
||||
(const uint8_t*)y.bytes, ys,
|
||||
(const uint8_t*)cb.bytes, cbs,
|
||||
(const uint8_t*)cr.bytes, crs,
|
||||
alpha_buf.data(), _paddedWidth,
|
||||
idx, &nal);
|
||||
|
||||
if (!result) {
|
||||
if (error) *error = makeError(@"encode failed");
|
||||
return NO;
|
||||
}
|
||||
|
||||
_nalFrames.push_back(std::move(nal));
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable NSData *)buildStreamWithError:(NSError **)error {
|
||||
if (!_encoder) {
|
||||
if (error) *error = makeError(@"Not in encoder mode");
|
||||
return nil;
|
||||
}
|
||||
|
||||
int paddedMbs = _paddedWidth / 16;
|
||||
FrameParams fp;
|
||||
fp.width_mbs = paddedMbs * 2; // double-wide canvas (color + alpha)
|
||||
fp.height_mbs = paddedMbs;
|
||||
fp.qp = _qp;
|
||||
fp.log2_max_frame_num = 4;
|
||||
|
||||
uint8_t hdr[128];
|
||||
size_t hdr_size = frame_writer::write_headers({hdr, sizeof(hdr)}, fp);
|
||||
|
||||
size_t total = hdr_size;
|
||||
for (auto& nal : _nalFrames) total += nal.size();
|
||||
|
||||
NSMutableData* stream = [NSMutableData dataWithLength:total];
|
||||
uint8_t* dst = (uint8_t*)stream.mutableBytes;
|
||||
memcpy(dst, hdr, hdr_size);
|
||||
size_t off = hdr_size;
|
||||
for (auto& nal : _nalFrames) {
|
||||
memcpy(dst + off, nal.data(), nal.size());
|
||||
off += nal.size();
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
@end
|
||||
18
third-party/subcodec/Sources/SubcodecObjC/SCSpriteRegion.mm
vendored
Normal file
18
third-party/subcodec/Sources/SubcodecObjC/SCSpriteRegion.mm
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Sources/SubcodecObjC/SCSpriteRegion.mm
|
||||
#import "SCSpriteRegion.h"
|
||||
|
||||
@implementation SCSpriteRegion
|
||||
|
||||
- (instancetype)initWithSlot:(int)slot
|
||||
colorRect:(CGRect)colorRect
|
||||
alphaRect:(CGRect)alphaRect {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_slot = slot;
|
||||
_colorRect = colorRect;
|
||||
_alphaRect = alphaRect;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
306
third-party/subcodec/Sources/SubcodecObjC/SCVideoToolboxDecoder.mm
vendored
Normal file
306
third-party/subcodec/Sources/SubcodecObjC/SCVideoToolboxDecoder.mm
vendored
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
// Sources/SubcodecObjC/SCVideoToolboxDecoder.mm
|
||||
#import "SCVideoToolboxDecoder.h"
|
||||
#include "AnnexBSplitter.h"
|
||||
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
static NSString* const kErrorDomain = @"SCVideoToolboxDecoder";
|
||||
|
||||
static NSError* makeVTError(NSString* msg, OSStatus status) {
|
||||
return [NSError errorWithDomain:kErrorDomain code:status
|
||||
userInfo:@{NSLocalizedDescriptionKey:
|
||||
[NSString stringWithFormat:@"%@ (OSStatus %d)", msg, (int)status]}];
|
||||
}
|
||||
|
||||
static NSError* makeError(NSString* msg) {
|
||||
return [NSError errorWithDomain:kErrorDomain code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||
}
|
||||
|
||||
// Returns the LAST occurrence of the target NAL type in the data.
|
||||
// This is important because buildStream() prepends subcodec's SPS/PPS before
|
||||
// OpenH264's SPS/PPS, and we need OpenH264's (the last ones) for VideoToolbox.
|
||||
static const uint8_t* findNAL(const uint8_t* data, size_t size,
|
||||
uint8_t targetType, size_t* nalSize) {
|
||||
const uint8_t* found = nullptr;
|
||||
size_t foundSize = 0;
|
||||
|
||||
for (size_t i = 0; i + 3 < size; ) {
|
||||
int sc_len = 0;
|
||||
if (i + 3 < size && data[i]==0 && data[i+1]==0 && data[i+2]==0 && data[i+3]==1)
|
||||
sc_len = 4;
|
||||
else if (i + 2 < size && data[i]==0 && data[i+1]==0 && data[i+2]==1)
|
||||
sc_len = 3;
|
||||
|
||||
if (sc_len > 0) {
|
||||
const uint8_t* nalStart = data + i + sc_len;
|
||||
uint8_t nal_type = nalStart[0] & 0x1F;
|
||||
|
||||
size_t nalEnd = size;
|
||||
for (size_t j = i + sc_len + 1; j + 2 < size; j++) {
|
||||
if (data[j]==0 && data[j+1]==0 &&
|
||||
(data[j+2]==1 || (j + 3 < size && data[j+2]==0 && data[j+3]==1))) {
|
||||
nalEnd = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nal_type == targetType) {
|
||||
found = nalStart;
|
||||
foundSize = nalEnd - (i + sc_len);
|
||||
}
|
||||
|
||||
i = nalEnd;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
*nalSize = foundSize;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> annexBToAVCC(const uint8_t* data, size_t size) {
|
||||
std::vector<uint8_t> avcc;
|
||||
avcc.reserve(size);
|
||||
|
||||
for (size_t i = 0; i + 3 < size; ) {
|
||||
int sc_len = 0;
|
||||
if (i + 3 < size && data[i]==0 && data[i+1]==0 && data[i+2]==0 && data[i+3]==1)
|
||||
sc_len = 4;
|
||||
else if (i + 2 < size && data[i]==0 && data[i+1]==0 && data[i+2]==1)
|
||||
sc_len = 3;
|
||||
|
||||
if (sc_len > 0) {
|
||||
size_t nalStart = i + sc_len;
|
||||
|
||||
size_t nalEnd = size;
|
||||
for (size_t j = nalStart + 1; j + 2 < size; j++) {
|
||||
if (data[j]==0 && data[j+1]==0 &&
|
||||
(data[j+2]==1 || (j + 3 < size && data[j+2]==0 && data[j+3]==1))) {
|
||||
nalEnd = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t nal_type = data[nalStart] & 0x1F;
|
||||
if (nal_type != 7 && nal_type != 8) {
|
||||
uint32_t nalLen = (uint32_t)(nalEnd - nalStart);
|
||||
uint8_t lenBuf[4] = {
|
||||
(uint8_t)(nalLen >> 24), (uint8_t)(nalLen >> 16),
|
||||
(uint8_t)(nalLen >> 8), (uint8_t)(nalLen)
|
||||
};
|
||||
avcc.insert(avcc.end(), lenBuf, lenBuf + 4);
|
||||
avcc.insert(avcc.end(), data + nalStart, data + nalEnd);
|
||||
}
|
||||
|
||||
i = nalEnd;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return avcc;
|
||||
}
|
||||
|
||||
struct DecodeContext {
|
||||
NSMutableArray<SCDecodedFrame *>* frames;
|
||||
};
|
||||
|
||||
static void decompressionCallback(void* decompressionOutputRefCon,
|
||||
void* sourceFrameRefCon,
|
||||
OSStatus status,
|
||||
VTDecodeInfoFlags infoFlags,
|
||||
CVImageBufferRef imageBuffer,
|
||||
CMTime presentationTimeStamp,
|
||||
CMTime presentationDuration) {
|
||||
if (status != noErr || !imageBuffer) return;
|
||||
|
||||
DecodeContext* ctx = (DecodeContext*)decompressionOutputRefCon;
|
||||
|
||||
CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)imageBuffer;
|
||||
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
int w = (int)CVPixelBufferGetWidth(pixelBuffer);
|
||||
int h = (int)CVPixelBufferGetHeight(pixelBuffer);
|
||||
|
||||
OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
|
||||
|
||||
NSMutableData* yData = [NSMutableData dataWithLength:w * h];
|
||||
NSMutableData* cbData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
|
||||
NSMutableData* crData = [NSMutableData dataWithLength:(w / 2) * (h / 2)];
|
||||
|
||||
uint8_t* yDst = (uint8_t*)yData.mutableBytes;
|
||||
uint8_t* cbDst = (uint8_t*)cbData.mutableBytes;
|
||||
uint8_t* crDst = (uint8_t*)crData.mutableBytes;
|
||||
|
||||
if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange ||
|
||||
pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
|
||||
uint8_t* yPlane = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
uint8_t* uvPlane = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
|
||||
for (int r = 0; r < h; r++)
|
||||
memcpy(yDst + r * w, yPlane + r * yStride, w);
|
||||
|
||||
int cw = w / 2;
|
||||
int ch = h / 2;
|
||||
for (int r = 0; r < ch; r++) {
|
||||
const uint8_t* uvRow = uvPlane + r * uvStride;
|
||||
for (int c = 0; c < cw; c++) {
|
||||
cbDst[r * cw + c] = uvRow[c * 2];
|
||||
crDst[r * cw + c] = uvRow[c * 2 + 1];
|
||||
}
|
||||
}
|
||||
} else if (pixelFormat == kCVPixelFormatType_420YpCbCr8Planar) {
|
||||
for (int p = 0; p < 3; p++) {
|
||||
uint8_t* src = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, p);
|
||||
size_t stride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, p);
|
||||
int pw = (p == 0) ? w : w / 2;
|
||||
int ph = (p == 0) ? h : h / 2;
|
||||
uint8_t* dst = (p == 0) ? yDst : (p == 1) ? cbDst : crDst;
|
||||
for (int r = 0; r < ph; r++)
|
||||
memcpy(dst + r * pw, src + r * stride, pw);
|
||||
}
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
SCDecodedFrame* frame = [[SCDecodedFrame alloc] initWithWidth:w height:h
|
||||
y:yData cb:cbData cr:crData];
|
||||
[ctx->frames addObject:frame];
|
||||
}
|
||||
|
||||
@implementation SCVideoToolboxDecoder
|
||||
|
||||
+ (nullable SCVideoToolboxDecoder *)createDecoderWithError:(NSError **)error {
|
||||
return [[SCVideoToolboxDecoder alloc] init];
|
||||
}
|
||||
|
||||
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
|
||||
error:(NSError **)error {
|
||||
const uint8_t* bytes = (const uint8_t*)data.bytes;
|
||||
size_t length = data.length;
|
||||
|
||||
auto packets = split_annex_b_frames(bytes, length);
|
||||
if (packets.empty()) {
|
||||
if (error) *error = makeError(@"No frames found in stream");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t spsSize = 0, ppsSize = 0;
|
||||
const uint8_t* spsNAL = findNAL(packets[0].data, packets[0].size, 7, &spsSize);
|
||||
const uint8_t* ppsNAL = findNAL(packets[0].data, packets[0].size, 8, &ppsSize);
|
||||
|
||||
if (!spsNAL || !ppsNAL) {
|
||||
if (error) *error = makeError(@"SPS or PPS not found in stream");
|
||||
return nil;
|
||||
}
|
||||
|
||||
const uint8_t* paramSets[2] = { spsNAL, ppsNAL };
|
||||
size_t paramSizes[2] = { spsSize, ppsSize };
|
||||
|
||||
CMVideoFormatDescriptionRef formatDesc = NULL;
|
||||
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(
|
||||
kCFAllocatorDefault, 2, paramSets, paramSizes, 4, &formatDesc);
|
||||
|
||||
if (status != noErr) {
|
||||
if (error) *error = makeVTError(@"CMVideoFormatDescriptionCreateFromH264ParameterSets failed", status);
|
||||
return nil;
|
||||
}
|
||||
|
||||
DecodeContext ctx;
|
||||
ctx.frames = [NSMutableArray array];
|
||||
|
||||
VTDecompressionOutputCallbackRecord callbackRecord;
|
||||
callbackRecord.decompressionOutputCallback = decompressionCallback;
|
||||
callbackRecord.decompressionOutputRefCon = &ctx;
|
||||
|
||||
NSDictionary* destAttrs = @{
|
||||
(NSString*)kCVPixelBufferPixelFormatTypeKey:
|
||||
@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
|
||||
};
|
||||
|
||||
VTDecompressionSessionRef session = NULL;
|
||||
status = VTDecompressionSessionCreate(
|
||||
kCFAllocatorDefault, formatDesc, NULL,
|
||||
(__bridge CFDictionaryRef)destAttrs,
|
||||
&callbackRecord, &session);
|
||||
|
||||
if (status != noErr) {
|
||||
CFRelease(formatDesc);
|
||||
if (error) *error = makeVTError(@"VTDecompressionSessionCreate failed", status);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError* decodeError = nil;
|
||||
|
||||
for (auto& pkt : packets) {
|
||||
auto avcc = annexBToAVCC(pkt.data, pkt.size);
|
||||
if (avcc.empty()) continue;
|
||||
|
||||
CMBlockBufferRef blockBuf = NULL;
|
||||
status = CMBlockBufferCreateWithMemoryBlock(
|
||||
kCFAllocatorDefault, NULL, avcc.size(), kCFAllocatorDefault,
|
||||
NULL, 0, avcc.size(), kCMBlockBufferAssureMemoryNowFlag, &blockBuf);
|
||||
|
||||
if (status != noErr) {
|
||||
decodeError = makeVTError(@"CMBlockBufferCreateWithMemoryBlock failed", status);
|
||||
break;
|
||||
}
|
||||
|
||||
status = CMBlockBufferReplaceDataBytes(avcc.data(), blockBuf, 0, avcc.size());
|
||||
if (status != noErr) {
|
||||
CFRelease(blockBuf);
|
||||
decodeError = makeVTError(@"CMBlockBufferReplaceDataBytes failed", status);
|
||||
break;
|
||||
}
|
||||
|
||||
CMSampleBufferRef sampleBuf = NULL;
|
||||
size_t sampleSize = avcc.size();
|
||||
status = CMSampleBufferCreate(
|
||||
kCFAllocatorDefault, blockBuf, true, NULL, NULL,
|
||||
formatDesc, 1, 0, NULL, 1, &sampleSize, &sampleBuf);
|
||||
|
||||
CFRelease(blockBuf);
|
||||
|
||||
if (status != noErr) {
|
||||
decodeError = makeVTError(@"CMSampleBufferCreate failed", status);
|
||||
break;
|
||||
}
|
||||
|
||||
VTDecodeInfoFlags flagsOut = 0;
|
||||
status = VTDecompressionSessionDecodeFrame(
|
||||
session, sampleBuf,
|
||||
kVTDecodeFrame_1xRealTimePlayback,
|
||||
NULL, &flagsOut);
|
||||
CFRelease(sampleBuf);
|
||||
|
||||
if (status != noErr) {
|
||||
decodeError = makeVTError(@"VTDecompressionSessionDecodeFrame failed", status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
VTDecompressionSessionWaitForAsynchronousFrames(session);
|
||||
|
||||
VTDecompressionSessionInvalidate(session);
|
||||
CFRelease(session);
|
||||
CFRelease(formatDesc);
|
||||
|
||||
if (decodeError) {
|
||||
if (error) *error = decodeError;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ctx.frames;
|
||||
}
|
||||
|
||||
@end
|
||||
22
third-party/subcodec/Sources/SubcodecObjC/include/SCDecodedFrame.h
vendored
Normal file
22
third-party/subcodec/Sources/SubcodecObjC/include/SCDecodedFrame.h
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Sources/SubcodecObjC/include/SCDecodedFrame.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCDecodedFrame : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int width;
|
||||
@property (nonatomic, readonly) int height;
|
||||
@property (nonatomic, readonly) NSData *y;
|
||||
@property (nonatomic, readonly) NSData *cb;
|
||||
@property (nonatomic, readonly) NSData *cr;
|
||||
|
||||
- (instancetype)initWithWidth:(int)width
|
||||
height:(int)height
|
||||
y:(NSData *)y
|
||||
cb:(NSData *)cb
|
||||
cr:(NSData *)cr;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
16
third-party/subcodec/Sources/SubcodecObjC/include/SCDecoding.h
vendored
Normal file
16
third-party/subcodec/Sources/SubcodecObjC/include/SCDecoding.h
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Sources/SubcodecObjC/include/SCDecoding.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SCDecodedFrame.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol SCDecoding <NSObject>
|
||||
|
||||
+ (nullable id<SCDecoding>)createDecoderWithError:(NSError **)error;
|
||||
|
||||
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
63
third-party/subcodec/Sources/SubcodecObjC/include/SCMuxSurface.h
vendored
Normal file
63
third-party/subcodec/Sources/SubcodecObjC/include/SCMuxSurface.h
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Sources/SubcodecObjC/include/SCMuxSurface.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SCSpriteRegion.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCCompactionInfo : NSObject
|
||||
@property (nonatomic, readonly) int activeSprites;
|
||||
@property (nonatomic, readonly) int maxSlots;
|
||||
@property (nonatomic, readonly) int currentGridMbs;
|
||||
@property (nonatomic, readonly) int minGridMbs;
|
||||
- (instancetype)initWithActiveSprites:(int)active
|
||||
maxSlots:(int)max
|
||||
currentGridMbs:(int)current
|
||||
minGridMbs:(int)min;
|
||||
@end
|
||||
|
||||
@interface SCResizeResult : NSObject
|
||||
@property (nonatomic, readonly) NSArray<SCSpriteRegion *> *regions;
|
||||
- (instancetype)initWithRegions:(NSArray<SCSpriteRegion *> *)regions;
|
||||
@end
|
||||
|
||||
@interface SCMuxSurface : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int widthMbs;
|
||||
@property (nonatomic, readonly) int heightMbs;
|
||||
|
||||
+ (nullable SCMuxSurface *)createWithSpriteWidth:(int)width
|
||||
spriteHeight:(int)height
|
||||
maxSlots:(int)slots
|
||||
qp:(int)qp
|
||||
sink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error;
|
||||
|
||||
- (nullable SCSpriteRegion *)addSpriteAtPath:(NSString *)path
|
||||
error:(NSError **)error;
|
||||
|
||||
- (void)removeSpriteAtSlot:(int)slot;
|
||||
|
||||
- (void)advanceSpriteAtSlot:(int)slot;
|
||||
- (BOOL)emitFrameIfNeededWithSink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error;
|
||||
|
||||
- (BOOL)advanceFrameWithSink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error;
|
||||
|
||||
- (nullable SCResizeResult *)resizeToMaxSlots:(int)newMaxSlots
|
||||
yPlane:(NSData *)yPlane
|
||||
cbPlane:(NSData *)cbPlane
|
||||
crPlane:(NSData *)crPlane
|
||||
decodedWidth:(int)decodedWidth
|
||||
decodedHeight:(int)decodedHeight
|
||||
strideY:(int)strideY
|
||||
strideCb:(int)strideCb
|
||||
strideCr:(int)strideCr
|
||||
withSink:(void (^)(NSData *))sink
|
||||
error:(NSError **)error;
|
||||
|
||||
- (SCCompactionInfo *)checkCompactionOpportunity;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
17
third-party/subcodec/Sources/SubcodecObjC/include/SCOpenH264Decoder.h
vendored
Normal file
17
third-party/subcodec/Sources/SubcodecObjC/include/SCOpenH264Decoder.h
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Sources/SubcodecObjC/include/SCOpenH264Decoder.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SCDecoding.h"
|
||||
#import "SCDecodedFrame.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCOpenH264Decoder : NSObject <SCDecoding>
|
||||
|
||||
+ (nullable SCOpenH264Decoder *)createDecoderWithError:(NSError **)error;
|
||||
|
||||
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
38
third-party/subcodec/Sources/SubcodecObjC/include/SCSprite.h
vendored
Normal file
38
third-party/subcodec/Sources/SubcodecObjC/include/SCSprite.h
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Sources/SubcodecObjC/include/SCSprite.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCSprite : NSObject
|
||||
|
||||
// SpriteExtractor path: raw YUV → .mbs file on disk
|
||||
+ (nullable SCSprite *)extractorWithSpriteSize:(int)size
|
||||
qp:(int)qp
|
||||
outputPath:(NSString *)path
|
||||
error:(NSError **)error;
|
||||
|
||||
- (BOOL)addFrameY:(NSData *)y yStride:(int)ys
|
||||
cb:(NSData *)cb cbStride:(int)cbs
|
||||
cr:(NSData *)cr crStride:(int)crs
|
||||
alpha:(NSData *)alpha alphaStride:(int)as
|
||||
error:(NSError **)error;
|
||||
|
||||
- (BOOL)finalizeExtraction:(NSError **)error;
|
||||
|
||||
// SpriteEncoder path: raw YUV → NAL data in memory (for reference decode)
|
||||
+ (nullable SCSprite *)encoderWithWidth:(int)width
|
||||
height:(int)height
|
||||
qp:(int)qp
|
||||
error:(NSError **)error;
|
||||
|
||||
- (BOOL)encodeFrameY:(NSData *)y yStride:(int)ys
|
||||
cb:(NSData *)cb cbStride:(int)cbs
|
||||
cr:(NSData *)cr crStride:(int)crs
|
||||
frameIndex:(int)idx
|
||||
error:(NSError **)error;
|
||||
|
||||
- (nullable NSData *)buildStreamWithError:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
19
third-party/subcodec/Sources/SubcodecObjC/include/SCSpriteRegion.h
vendored
Normal file
19
third-party/subcodec/Sources/SubcodecObjC/include/SCSpriteRegion.h
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Sources/SubcodecObjC/include/SCSpriteRegion.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CGGeometry.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCSpriteRegion : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int slot;
|
||||
@property (nonatomic, readonly) CGRect colorRect;
|
||||
@property (nonatomic, readonly) CGRect alphaRect;
|
||||
|
||||
- (instancetype)initWithSlot:(int)slot
|
||||
colorRect:(CGRect)colorRect
|
||||
alphaRect:(CGRect)alphaRect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
17
third-party/subcodec/Sources/SubcodecObjC/include/SCVideoToolboxDecoder.h
vendored
Normal file
17
third-party/subcodec/Sources/SubcodecObjC/include/SCVideoToolboxDecoder.h
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Sources/SubcodecObjC/include/SCVideoToolboxDecoder.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SCDecoding.h"
|
||||
#import "SCDecodedFrame.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCVideoToolboxDecoder : NSObject <SCDecoding>
|
||||
|
||||
+ (nullable SCVideoToolboxDecoder *)createDecoderWithError:(NSError **)error;
|
||||
|
||||
- (nullable NSArray<SCDecodedFrame *> *)decodeStream:(NSData *)data
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
8
third-party/subcodec/Sources/SubcodecObjC/include/SubcodecObjC.h
vendored
Normal file
8
third-party/subcodec/Sources/SubcodecObjC/include/SubcodecObjC.h
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Sources/SubcodecObjC/include/SubcodecObjC.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SCDecodedFrame.h"
|
||||
#import "SCSprite.h"
|
||||
#import "SCDecoding.h"
|
||||
#import "SCOpenH264Decoder.h"
|
||||
#import "SCMuxSurface.h"
|
||||
#import "SCVideoToolboxDecoder.h"
|
||||
1061
third-party/subcodec/Tests/SubcodecTests/MuxSurfaceTests.swift
vendored
Normal file
1061
third-party/subcodec/Tests/SubcodecTests/MuxSurfaceTests.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
65
third-party/subcodec/Tests/SubcodecTests/generate_fixtures.cpp
vendored
Normal file
65
third-party/subcodec/Tests/SubcodecTests/generate_fixtures.cpp
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Tests/SubcodecTests/generate_fixtures.cpp
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#define SPRITE_PX 64
|
||||
#define NUM_FRAMES 160
|
||||
|
||||
static void generate_sprite_frame(uint8_t* y_plane, uint8_t* cb_plane, uint8_t* cr_plane,
|
||||
int sprite_id, int frame) {
|
||||
uint8_t cb_val = (uint8_t)(128 + sprite_id * 20);
|
||||
uint8_t cr_val = (uint8_t)(128 - sprite_id * 20);
|
||||
|
||||
for (int py = 0; py < SPRITE_PX; py++) {
|
||||
for (int px = 0; px < SPRITE_PX; px++) {
|
||||
uint8_t y_val;
|
||||
switch (sprite_id) {
|
||||
case 0: y_val = (uint8_t)((px + frame * 8) % 256); break;
|
||||
case 1: y_val = (uint8_t)((py + frame * 8) % 256); break;
|
||||
case 2: y_val = (uint8_t)((px + py + frame * 8) % 256); break;
|
||||
default: y_val = 128; break;
|
||||
}
|
||||
y_plane[py * SPRITE_PX + px] = y_val;
|
||||
}
|
||||
}
|
||||
|
||||
for (int cy = 0; cy < SPRITE_PX / 2; cy++) {
|
||||
for (int cx = 0; cx < SPRITE_PX / 2; cx++) {
|
||||
cb_plane[cy * (SPRITE_PX / 2) + cx] = cb_val;
|
||||
cr_plane[cy * (SPRITE_PX / 2) + cx] = cr_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Usage: %s <output_dir>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* dir = argv[1];
|
||||
const int y_size = SPRITE_PX * SPRITE_PX;
|
||||
const int c_size = (SPRITE_PX / 2) * (SPRITE_PX / 2);
|
||||
|
||||
uint8_t y[y_size], cb[c_size], cr[c_size];
|
||||
|
||||
for (int s = 0; s < 3; s++) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/sprite%d.yuv", dir, s);
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) { perror(path); return 1; }
|
||||
|
||||
for (int frame = 0; frame < NUM_FRAMES; frame++) {
|
||||
generate_sprite_frame(y, cb, cr, s, frame);
|
||||
fwrite(y, 1, y_size, f);
|
||||
fwrite(cb, 1, c_size, f);
|
||||
fwrite(cr, 1, c_size, f);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
printf("Wrote %s (%d frames, %d bytes)\n", path, NUM_FRAMES,
|
||||
NUM_FRAMES * (y_size + c_size + c_size));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
BIN
third-party/subcodec/demo/input.mp4
vendored
Normal file
BIN
third-party/subcodec/demo/input.mp4
vendored
Normal file
Binary file not shown.
856
third-party/subcodec/src/cavlc.cpp
vendored
Normal file
856
third-party/subcodec/src/cavlc.cpp
vendored
Normal file
|
|
@ -0,0 +1,856 @@
|
|||
#include "cavlc.h"
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
namespace subcodec::cavlc {
|
||||
|
||||
// coeff_token VLC tables from H.264 spec Table 9-5
|
||||
// Format: {code, length} for each (TotalCoeff, TrailingOnes) pair
|
||||
// Indexed by [trailing_ones][total_coeff]
|
||||
|
||||
struct vlc_t {
|
||||
uint16_t code;
|
||||
uint8_t len;
|
||||
};
|
||||
|
||||
// Table 9-5(a): 0 <= nC < 2
|
||||
// Indexed by [trailing_ones][total_coeff]
|
||||
// trailing_ones: 0-3, total_coeff: 0-16
|
||||
static const vlc_t coeff_token_table_0[4][17] = {
|
||||
// TrailingOnes = 0
|
||||
{
|
||||
{0x01, 1}, {0x05, 6}, {0x07, 8}, {0x07, 9}, {0x07, 10}, {0x07, 11}, {0x0F, 13}, {0x0B, 13}, {0x08, 13},
|
||||
{0x0F, 14}, {0x0B, 14}, {0x0F, 15}, {0x0B, 15}, {0x0F, 16}, {0x0B, 16}, {0x07, 16}, {0x04, 16},
|
||||
},
|
||||
// TrailingOnes = 1
|
||||
{
|
||||
{0x00, 0}, {0x01, 2}, {0x04, 6}, {0x06, 8}, {0x06, 9}, {0x06, 10}, {0x06, 11}, {0x0E, 13}, {0x0A, 13},
|
||||
{0x0E, 14}, {0x0A, 14}, {0x0E, 15}, {0x0A, 15}, {0x01, 15}, {0x0E, 16}, {0x0A, 16}, {0x06, 16},
|
||||
},
|
||||
// TrailingOnes = 2
|
||||
{
|
||||
{0x00, 0}, {0x00, 0}, {0x01, 3}, {0x05, 7}, {0x05, 8}, {0x05, 9}, {0x05, 10}, {0x05, 11}, {0x0D, 13},
|
||||
{0x09, 13}, {0x0D, 14}, {0x09, 14}, {0x0D, 15}, {0x09, 15}, {0x0D, 16}, {0x09, 16}, {0x05, 16},
|
||||
},
|
||||
// TrailingOnes = 3
|
||||
{
|
||||
{0x00, 0}, {0x00, 0}, {0x00, 0}, {0x03, 5}, {0x03, 6}, {0x04, 7}, {0x04, 8}, {0x04, 9}, {0x04, 10},
|
||||
{0x04, 11}, {0x0C, 13}, {0x0C, 14}, {0x08, 14}, {0x0C, 15}, {0x08, 15}, {0x0C, 16}, {0x08, 16},
|
||||
},
|
||||
};
|
||||
|
||||
// Table 9-5(b): 2 <= nC < 4
|
||||
static const vlc_t coeff_token_table_2[4][17] = {
|
||||
// TrailingOnes = 0
|
||||
{
|
||||
{0x03, 2}, {0x0B, 6}, {0x07, 6}, {0x07, 7}, {0x07, 8}, {0x04, 8}, {0x07, 9}, {0x0F, 11}, {0x0B, 11},
|
||||
{0x0F, 12}, {0x0B, 12}, {0x08, 12}, {0x0F, 13}, {0x0B, 13}, {0x07, 13}, {0x09, 14}, {0x07, 14},
|
||||
},
|
||||
// TrailingOnes = 1
|
||||
{
|
||||
{0x00, 0}, {0x02, 2}, {0x07, 5}, {0x0A, 6}, {0x06, 6}, {0x06, 7}, {0x06, 8}, {0x06, 9}, {0x0E, 11},
|
||||
{0x0A, 11}, {0x0E, 12}, {0x0A, 12}, {0x0E, 13}, {0x0A, 13}, {0x0B, 14}, {0x08, 14}, {0x06, 14},
|
||||
},
|
||||
// TrailingOnes = 2
|
||||
{
|
||||
{0x00, 0}, {0x00, 0}, {0x03, 3}, {0x09, 6}, {0x05, 6}, {0x05, 7}, {0x05, 8}, {0x05, 9}, {0x0D, 11},
|
||||
{0x09, 11}, {0x0D, 12}, {0x09, 12}, {0x0D, 13}, {0x09, 13}, {0x06, 13}, {0x0A, 14}, {0x05, 14},
|
||||
},
|
||||
// TrailingOnes = 3
|
||||
{
|
||||
{0x00, 0}, {0x00, 0}, {0x00, 0}, {0x05, 4}, {0x04, 4}, {0x06, 5}, {0x08, 6}, {0x04, 6}, {0x04, 7},
|
||||
{0x04, 9}, {0x0C, 11}, {0x08, 11}, {0x0C, 12}, {0x0C, 13}, {0x08, 13}, {0x01, 13}, {0x04, 14},
|
||||
},
|
||||
};
|
||||
|
||||
// Table 9-5(c): 4 <= nC < 8
|
||||
static const vlc_t coeff_token_table_4[4][17] = {
|
||||
// TrailingOnes = 0
|
||||
{
|
||||
{0x0F, 4}, {0x0F, 6}, {0x0B, 6}, {0x08, 6}, {0x0F, 7}, {0x0B, 7}, {0x09, 7}, {0x08, 7}, {0x0F, 8},
|
||||
{0x0B, 8}, {0x0F, 9}, {0x0B, 9}, {0x08, 9}, {0x0D, 10}, {0x09, 10}, {0x05, 10}, {0x01, 10},
|
||||
},
|
||||
// TrailingOnes = 1
|
||||
{
|
||||
{0x00, 0}, {0x0E, 4}, {0x0F, 5}, {0x0C, 5}, {0x0A, 5}, {0x08, 5}, {0x0E, 6}, {0x0A, 6}, {0x0E, 7},
|
||||
{0x0E, 8}, {0x0A, 8}, {0x0E, 9}, {0x0A, 9}, {0x07, 9}, {0x0C, 10}, {0x08, 10}, {0x04, 10},
|
||||
},
|
||||
// TrailingOnes = 2
|
||||
{
|
||||
{0x00, 0}, {0x00, 0}, {0x0D, 4}, {0x0E, 5}, {0x0B, 5}, {0x09, 5}, {0x0D, 6}, {0x09, 6}, {0x0D, 7},
|
||||
{0x0A, 7}, {0x0D, 8}, {0x09, 8}, {0x0D, 9}, {0x09, 9}, {0x0B, 10}, {0x07, 10}, {0x03, 10},
|
||||
},
|
||||
// TrailingOnes = 3
|
||||
{
|
||||
{0x00, 0}, {0x00, 0}, {0x00, 0}, {0x0C, 4}, {0x0B, 4}, {0x0A, 4}, {0x09, 4}, {0x08, 4}, {0x0D, 5},
|
||||
{0x0C, 6}, {0x0C, 7}, {0x0C, 8}, {0x08, 8}, {0x0C, 9}, {0x0A, 10}, {0x06, 10}, {0x02, 10},
|
||||
},
|
||||
};
|
||||
|
||||
// Table 9-5(e): ChromaDCLevel (nC == -1), 4:2:0
|
||||
// Chroma DC has max 4 coefficients
|
||||
static const vlc_t coeff_token_chroma_dc[4][5] = {
|
||||
// TrailingOnes = 0
|
||||
{
|
||||
{0x01, 2}, // TotalCoeff = 0
|
||||
{0x07, 6}, // TotalCoeff = 1
|
||||
{0x04, 6}, // TotalCoeff = 2
|
||||
{0x03, 6}, // TotalCoeff = 3
|
||||
{0x02, 6}, // TotalCoeff = 4
|
||||
},
|
||||
// TrailingOnes = 1
|
||||
{
|
||||
{0x00, 0}, // TotalCoeff = 0 (invalid)
|
||||
{0x01, 1}, // TotalCoeff = 1
|
||||
{0x06, 6}, // TotalCoeff = 2
|
||||
{0x03, 7}, // TotalCoeff = 3
|
||||
{0x03, 8}, // TotalCoeff = 4
|
||||
},
|
||||
// TrailingOnes = 2
|
||||
{
|
||||
{0x00, 0}, // TotalCoeff = 0 (invalid)
|
||||
{0x00, 0}, // TotalCoeff = 1 (invalid)
|
||||
{0x01, 3}, // TotalCoeff = 2
|
||||
{0x02, 7}, // TotalCoeff = 3
|
||||
{0x02, 8}, // TotalCoeff = 4
|
||||
},
|
||||
// TrailingOnes = 3
|
||||
{
|
||||
{0x00, 0}, // TotalCoeff = 0 (invalid)
|
||||
{0x00, 0}, // TotalCoeff = 1 (invalid)
|
||||
{0x00, 0}, // TotalCoeff = 2 (invalid)
|
||||
{0x05, 6}, // TotalCoeff = 3
|
||||
{0x00, 7}, // TotalCoeff = 4
|
||||
},
|
||||
};
|
||||
|
||||
// Table 9-7: total_zeros for 4x4 blocks (chroma AC or luma)
|
||||
// Indexed by [total_coeff - 1][total_zeros]
|
||||
// total_coeff ranges from 1 to 15 (0 coeffs means no total_zeros coded)
|
||||
// total_zeros ranges from 0 to (16 - total_coeff)
|
||||
static const vlc_t total_zeros_table[15][16] = {
|
||||
// total_coeff = 1: total_zeros can be 0-15
|
||||
{{0x1, 1}, {0x3, 3}, {0x2, 3}, {0x3, 4}, {0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6},
|
||||
{0x2, 6}, {0x3, 7}, {0x2, 7}, {0x3, 8}, {0x2, 8}, {0x3, 9}, {0x2, 9}, {0x1, 9}},
|
||||
// total_coeff = 2: total_zeros can be 0-14
|
||||
{{0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x5, 4}, {0x4, 4}, {0x3, 4},
|
||||
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6}, {0x2, 6}, {0x1, 6}, {0x0, 6}, {0x0, 0}},
|
||||
// total_coeff = 3: total_zeros can be 0-13
|
||||
{{0x5, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 4}, {0x3, 4}, {0x4, 3}, {0x3, 3},
|
||||
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x1, 6}, {0x1, 5}, {0x0, 6}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 4: total_zeros can be 0-12
|
||||
{{0x3, 5}, {0x7, 3}, {0x5, 4}, {0x4, 4}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 4},
|
||||
{0x3, 3}, {0x2, 4}, {0x2, 5}, {0x1, 5}, {0x0, 5}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 5: total_zeros can be 0-11
|
||||
{{0x5, 4}, {0x4, 4}, {0x3, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3},
|
||||
{0x2, 4}, {0x1, 5}, {0x1, 4}, {0x0, 5}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 6: total_zeros can be 0-10
|
||||
{{0x1, 6}, {0x1, 5}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x2, 3},
|
||||
{0x1, 4}, {0x1, 3}, {0x0, 6}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 7: total_zeros can be 0-9
|
||||
{{0x1, 6}, {0x1, 5}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x3, 2}, {0x2, 3}, {0x1, 4},
|
||||
{0x1, 3}, {0x0, 6}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 8: total_zeros can be 0-8
|
||||
{{0x1, 6}, {0x1, 4}, {0x1, 5}, {0x3, 3}, {0x3, 2}, {0x2, 2}, {0x2, 3}, {0x1, 3},
|
||||
{0x0, 6}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 9: total_zeros can be 0-7
|
||||
{{0x1, 6}, {0x0, 6}, {0x1, 4}, {0x3, 2}, {0x2, 2}, {0x1, 3}, {0x1, 2}, {0x1, 5},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 10: total_zeros can be 0-6
|
||||
{{0x1, 5}, {0x0, 5}, {0x1, 3}, {0x3, 2}, {0x2, 2}, {0x1, 2}, {0x1, 4},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 11: total_zeros can be 0-5
|
||||
{{0x0, 4}, {0x1, 4}, {0x1, 3}, {0x2, 3}, {0x1, 1}, {0x3, 3},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 12: total_zeros can be 0-4
|
||||
{{0x0, 4}, {0x1, 4}, {0x1, 2}, {0x1, 1}, {0x1, 3},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 13: total_zeros can be 0-3
|
||||
{{0x0, 3}, {0x1, 3}, {0x1, 1}, {0x1, 2},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 14: total_zeros can be 0-2
|
||||
{{0x0, 2}, {0x1, 2}, {0x1, 1},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 15: total_zeros can be 0-1
|
||||
{{0x0, 1}, {0x1, 1},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
};
|
||||
|
||||
// Table 9-8: total_zeros for 15-coefficient blocks (I_16x16 AC blocks)
|
||||
// Indexed by [total_coeff - 1][total_zeros]
|
||||
// total_coeff ranges from 1 to 14 (0 coeffs means no total_zeros coded)
|
||||
// total_zeros ranges from 0 to (15 - total_coeff)
|
||||
static const vlc_t total_zeros_table_15[14][15] = {
|
||||
// total_coeff = 1: total_zeros can be 0-14
|
||||
{{0x1, 1}, {0x3, 3}, {0x2, 3}, {0x3, 4}, {0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6},
|
||||
{0x2, 6}, {0x3, 7}, {0x2, 7}, {0x3, 8}, {0x2, 8}, {0x3, 9}, {0x2, 9}},
|
||||
// total_coeff = 2: total_zeros can be 0-13
|
||||
{{0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x5, 4}, {0x4, 4}, {0x3, 4},
|
||||
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x3, 6}, {0x2, 6}, {0x1, 6}, {0x0, 0}},
|
||||
// total_coeff = 3: total_zeros can be 0-12
|
||||
{{0x5, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 4}, {0x3, 4}, {0x4, 3}, {0x3, 3},
|
||||
{0x2, 4}, {0x3, 5}, {0x2, 5}, {0x1, 6}, {0x1, 5}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 4: total_zeros can be 0-11
|
||||
{{0x3, 5}, {0x7, 3}, {0x5, 4}, {0x4, 4}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 4},
|
||||
{0x3, 3}, {0x2, 4}, {0x2, 5}, {0x1, 5}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 5: total_zeros can be 0-10
|
||||
{{0x5, 4}, {0x4, 4}, {0x3, 4}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3},
|
||||
{0x2, 4}, {0x1, 5}, {0x1, 4}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 6: total_zeros can be 0-9
|
||||
{{0x1, 6}, {0x1, 5}, {0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x2, 3},
|
||||
{0x1, 4}, {0x1, 3}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 7: total_zeros can be 0-8
|
||||
{{0x1, 6}, {0x1, 5}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x3, 2}, {0x2, 3}, {0x1, 4},
|
||||
{0x1, 3}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 8: total_zeros can be 0-7
|
||||
{{0x1, 6}, {0x1, 4}, {0x1, 5}, {0x3, 3}, {0x3, 2}, {0x2, 2}, {0x2, 3}, {0x1, 3},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 9: total_zeros can be 0-6
|
||||
{{0x1, 6}, {0x0, 6}, {0x1, 4}, {0x3, 2}, {0x2, 2}, {0x1, 3}, {0x1, 2},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 10: total_zeros can be 0-5
|
||||
{{0x1, 5}, {0x0, 5}, {0x1, 3}, {0x3, 2}, {0x2, 2}, {0x1, 2},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 11: total_zeros can be 0-4
|
||||
{{0x0, 4}, {0x1, 4}, {0x1, 3}, {0x2, 3}, {0x1, 1},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 12: total_zeros can be 0-3
|
||||
{{0x0, 4}, {0x1, 4}, {0x1, 2}, {0x1, 1},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 13: total_zeros can be 0-2
|
||||
{{0x0, 3}, {0x1, 3}, {0x1, 1},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// total_coeff = 14: total_zeros can be 0-1
|
||||
{{0x0, 2}, {0x1, 2},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
};
|
||||
|
||||
// Table 9-9: total_zeros for chroma DC 4:2:0
|
||||
// Indexed by [total_coeff - 1][total_zeros]
|
||||
// total_coeff ranges from 1 to 3 (max 4 coeffs, so if TotalCoeff=4, no zeros)
|
||||
// total_zeros ranges from 0 to (4 - total_coeff)
|
||||
static const vlc_t total_zeros_chroma_dc[3][4] = {
|
||||
// total_coeff = 1: total_zeros can be 0-3
|
||||
{{0x1, 1}, {0x1, 2}, {0x1, 3}, {0x0, 3}},
|
||||
// total_coeff = 2: total_zeros can be 0-2
|
||||
{{0x1, 1}, {0x1, 2}, {0x0, 2}, {0x0, 0}},
|
||||
// total_coeff = 3: total_zeros can be 0-1
|
||||
{{0x1, 1}, {0x0, 1}, {0x0, 0}, {0x0, 0}},
|
||||
};
|
||||
|
||||
// Table 9-10: run_before
|
||||
// Indexed by [min(zeros_left - 1, 6)][run_before]
|
||||
// zeros_left: remaining zeros to distribute
|
||||
// run_before: zeros before this coefficient (0 to zeros_left)
|
||||
static const vlc_t run_before_table[7][15] = {
|
||||
// zeros_left = 1
|
||||
{{0x1, 1}, {0x0, 1},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// zeros_left = 2
|
||||
{{0x1, 1}, {0x1, 2}, {0x0, 2},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// zeros_left = 3
|
||||
{{0x3, 2}, {0x2, 2}, {0x1, 2}, {0x0, 2},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// zeros_left = 4
|
||||
{{0x3, 2}, {0x2, 2}, {0x1, 2}, {0x1, 3}, {0x0, 3},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// zeros_left = 5
|
||||
{{0x3, 2}, {0x2, 2}, {0x3, 3}, {0x2, 3}, {0x1, 3}, {0x0, 3},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// zeros_left = 6
|
||||
{{0x3, 2}, {0x0, 3}, {0x1, 3}, {0x3, 3}, {0x2, 3}, {0x5, 3}, {0x4, 3},
|
||||
{0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}, {0x0, 0}},
|
||||
// zeros_left >= 7
|
||||
{{0x7, 3}, {0x6, 3}, {0x5, 3}, {0x4, 3}, {0x3, 3}, {0x2, 3}, {0x1, 3}, {0x1, 4}, {0x1, 5}, {0x1, 6}, {0x1, 7}, {0x1, 8}, {0x1, 9}, {0x1, 10}, {0x1, 11}},
|
||||
};
|
||||
|
||||
void write_coeff_token(bs_t* b, int total_coeff, int trailing_ones, int nc) {
|
||||
// Input validation
|
||||
assert(trailing_ones >= 0 && trailing_ones <= 3);
|
||||
assert(total_coeff >= 0);
|
||||
if (nc == -1) {
|
||||
assert(total_coeff <= 4); // Chroma DC 4:2:0 has max 4 coefficients
|
||||
} else {
|
||||
assert(total_coeff <= 16);
|
||||
}
|
||||
|
||||
const vlc_t* entry;
|
||||
|
||||
if (nc == -1) {
|
||||
// Chroma DC (4:2:0)
|
||||
entry = &coeff_token_chroma_dc[trailing_ones][total_coeff];
|
||||
} else if (nc < 2) {
|
||||
entry = &coeff_token_table_0[trailing_ones][total_coeff];
|
||||
} else if (nc < 4) {
|
||||
entry = &coeff_token_table_2[trailing_ones][total_coeff];
|
||||
} else if (nc < 8) {
|
||||
entry = &coeff_token_table_4[trailing_ones][total_coeff];
|
||||
} else {
|
||||
// nC >= 8: Table 9-5(d) - fixed-length 6-bit encoding
|
||||
// code=3 → tc=0; otherwise code = ((tc-1) << 2) | t1
|
||||
int code;
|
||||
if (total_coeff == 0) {
|
||||
code = 3;
|
||||
} else {
|
||||
code = ((total_coeff - 1) << 2) | trailing_ones;
|
||||
}
|
||||
bs_write_u(b, 6, code);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(entry->len > 0); // Invalid coeff_token combination
|
||||
bs_write_u(b, entry->len, entry->code);
|
||||
}
|
||||
|
||||
void write_level_prefix(bs_t* b, int level_prefix) {
|
||||
// level_prefix is encoded as level_prefix zeros followed by a 1
|
||||
for (int i = 0; i < level_prefix; i++) {
|
||||
bs_write_u1(b, 0);
|
||||
}
|
||||
bs_write_u1(b, 1);
|
||||
}
|
||||
|
||||
void write_total_zeros(bs_t* b, int total_zeros, int total_coeff, int max_num_coeff) {
|
||||
assert(total_coeff > 0);
|
||||
assert(total_zeros >= 0);
|
||||
|
||||
const vlc_t* entry;
|
||||
if (max_num_coeff == 4) {
|
||||
// Chroma DC 4:2:0
|
||||
assert(total_coeff <= 3);
|
||||
assert(total_zeros <= 4 - total_coeff);
|
||||
entry = &total_zeros_chroma_dc[total_coeff - 1][total_zeros];
|
||||
} else if (max_num_coeff == 15) {
|
||||
// AC blocks for I_16x16 (Table 9-8)
|
||||
assert(total_coeff <= 14);
|
||||
assert(total_zeros <= 15 - total_coeff);
|
||||
entry = &total_zeros_table_15[total_coeff - 1][total_zeros];
|
||||
} else if (max_num_coeff == 16) {
|
||||
// 4x4 luma block (full)
|
||||
assert(total_coeff <= 15);
|
||||
assert(total_zeros <= 16 - total_coeff);
|
||||
entry = &total_zeros_table[total_coeff - 1][total_zeros];
|
||||
} else {
|
||||
assert(0 && "Unsupported max_num_coeff value");
|
||||
return;
|
||||
}
|
||||
|
||||
assert(entry->len > 0);
|
||||
bs_write_u(b, entry->len, entry->code);
|
||||
}
|
||||
|
||||
void write_run_before(bs_t* b, int run_before, int zeros_left) {
|
||||
assert(zeros_left > 0);
|
||||
assert(run_before >= 0 && run_before <= zeros_left);
|
||||
|
||||
int idx = (zeros_left > 7) ? 6 : zeros_left - 1;
|
||||
const vlc_t* entry = &run_before_table[idx][run_before];
|
||||
|
||||
assert(entry->len > 0);
|
||||
bs_write_u(b, entry->len, entry->code);
|
||||
}
|
||||
|
||||
// Static helper for level encoding with suffix_length adaptation
|
||||
void write_level(bs_t* b, int level, int* suffix_length) {
|
||||
int sign = (level < 0) ? 1 : 0;
|
||||
int abs_level = sign ? -level : level;
|
||||
|
||||
// levelCode = 2 * abs_level - 2 + sign (for abs_level > 0)
|
||||
// But we've already adjusted for first non-T1 level
|
||||
int level_code = (abs_level << 1) - 2 + sign;
|
||||
|
||||
// Determine level_prefix and level_suffix
|
||||
int level_prefix;
|
||||
int level_suffix_size = *suffix_length;
|
||||
int level_suffix = 0;
|
||||
|
||||
if (*suffix_length == 0) {
|
||||
// level_prefix = min(14, level_code)
|
||||
// If level_code >= 14, level_suffix uses escape coding
|
||||
level_prefix = (level_code < 14) ? level_code : 14;
|
||||
if (level_code >= 14) {
|
||||
level_suffix_size = 4;
|
||||
level_suffix = level_code - 14;
|
||||
if (level_code >= 30) {
|
||||
// Extended escape
|
||||
level_prefix = 15;
|
||||
level_suffix_size = 12;
|
||||
level_suffix = level_code - 30;
|
||||
}
|
||||
} else {
|
||||
level_suffix_size = 0;
|
||||
}
|
||||
} else {
|
||||
// Normal case with suffix
|
||||
// Escape threshold: level_prefix 0..14 with suffixLength suffix bits
|
||||
// can represent levelCodes 0..(15 << suffixLength)-1
|
||||
int threshold = (15 << *suffix_length);
|
||||
if (level_code < threshold) {
|
||||
level_prefix = level_code >> *suffix_length;
|
||||
level_suffix = level_code & ((1 << *suffix_length) - 1);
|
||||
} else {
|
||||
// Escape: level_prefix = 15, 12-bit suffix
|
||||
level_prefix = 15;
|
||||
level_suffix_size = 12;
|
||||
level_suffix = level_code - threshold;
|
||||
}
|
||||
}
|
||||
|
||||
write_level_prefix(b, level_prefix);
|
||||
if (level_suffix_size > 0) {
|
||||
bs_write_u(b, level_suffix_size, level_suffix);
|
||||
}
|
||||
|
||||
// NOTE: suffix_length update is done by the caller (write_block)
|
||||
// so it can use the original abs_level before first-non-T1 adjustment.
|
||||
}
|
||||
|
||||
// --- Read (decode) helpers ---
|
||||
|
||||
// Read coeff_token by searching VLC table for matching code
|
||||
// Sets *total_coeff and *trailing_ones, returns 0 on success
|
||||
static int read_coeff_token_vlc(bs_t* b, const vlc_t table[][17],
|
||||
int max_tc, int* total_coeff, int* trailing_ones) {
|
||||
// We need to match against variable-length codes.
|
||||
// Strategy: try all valid entries, find the one that matches the next bits.
|
||||
// Since codes are prefix-free, peek at enough bits and check each candidate.
|
||||
// Max code length in tables is 16 bits.
|
||||
uint32_t lookahead = bs_next_bits(b, 16);
|
||||
|
||||
for (int t1 = 0; t1 <= 3; t1++) {
|
||||
for (int tc = 0; tc <= max_tc; tc++) {
|
||||
if (t1 > tc) continue; // invalid: trailing_ones > total_coeff
|
||||
if (tc == 0 && t1 != 0) continue;
|
||||
const vlc_t* e = &table[t1][tc];
|
||||
if (e->len == 0) continue; // invalid entry
|
||||
uint32_t code = lookahead >> (16 - e->len);
|
||||
if (code == e->code) {
|
||||
*total_coeff = tc;
|
||||
*trailing_ones = t1;
|
||||
bs_skip_u(b, e->len);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1; // no match found
|
||||
}
|
||||
|
||||
static int read_coeff_token_chroma_dc(bs_t* b, int* total_coeff, int* trailing_ones) {
|
||||
uint32_t lookahead = bs_next_bits(b, 8);
|
||||
|
||||
for (int t1 = 0; t1 <= 3; t1++) {
|
||||
for (int tc = 0; tc <= 4; tc++) {
|
||||
if (t1 > tc) continue;
|
||||
if (tc == 0 && t1 != 0) continue;
|
||||
const vlc_t* e = &coeff_token_chroma_dc[t1][tc];
|
||||
if (e->len == 0) continue;
|
||||
uint32_t code = lookahead >> (8 - e->len);
|
||||
if (code == e->code) {
|
||||
*total_coeff = tc;
|
||||
*trailing_ones = t1;
|
||||
bs_skip_u(b, e->len);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int read_coeff_token(bs_t* b, int nc, int* total_coeff, int* trailing_ones) {
|
||||
if (nc == -1) {
|
||||
return read_coeff_token_chroma_dc(b, total_coeff, trailing_ones);
|
||||
} else if (nc >= 8) {
|
||||
// Fixed-length 6-bit code (H.264 Table 9-5(d))
|
||||
// code=3 → tc=0; otherwise tc = (code>>2)+1, t1 = code&3
|
||||
uint32_t code = bs_read_u(b, 6);
|
||||
if (code == 3) {
|
||||
*total_coeff = 0;
|
||||
*trailing_ones = 0;
|
||||
} else {
|
||||
*total_coeff = (int)(code >> 2) + 1;
|
||||
*trailing_ones = (int)(code & 3);
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
const vlc_t (*table)[17];
|
||||
if (nc < 2) table = coeff_token_table_0;
|
||||
else if (nc < 4) table = coeff_token_table_2;
|
||||
else table = coeff_token_table_4;
|
||||
return read_coeff_token_vlc(b, table, 16, total_coeff, trailing_ones);
|
||||
}
|
||||
}
|
||||
|
||||
// Read level_prefix (unary: count zeros before a 1)
|
||||
static int read_level_prefix(bs_t* b) {
|
||||
int prefix = 0;
|
||||
while (bs_read_u1(b) == 0 && !bs_eof(b)) {
|
||||
prefix++;
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
// Read a level value given current suffix_length, update suffix_length
|
||||
// Per H.264 spec section 9.2.2.1 (Table 9-6)
|
||||
static int read_level(bs_t* b, int* suffix_length) {
|
||||
int level_prefix = read_level_prefix(b);
|
||||
int level_code;
|
||||
int level_suffix_size;
|
||||
int level_suffix = 0;
|
||||
|
||||
// Determine suffix size per spec
|
||||
if (level_prefix == 14 && *suffix_length == 0) {
|
||||
level_suffix_size = 4;
|
||||
} else if (level_prefix >= 15) {
|
||||
level_suffix_size = level_prefix - 3;
|
||||
} else {
|
||||
level_suffix_size = *suffix_length;
|
||||
}
|
||||
|
||||
if (level_suffix_size > 0) {
|
||||
level_suffix = bs_read_u(b, level_suffix_size);
|
||||
}
|
||||
|
||||
// Compute levelCode per spec:
|
||||
// levelCode = (Min(15, level_prefix) << suffixLength) + level_suffix
|
||||
int capped_prefix = (level_prefix < 15) ? level_prefix : 15;
|
||||
level_code = (capped_prefix << *suffix_length) + level_suffix;
|
||||
|
||||
// Additional adjustments per spec
|
||||
if (level_prefix >= 15 && *suffix_length == 0) {
|
||||
level_code += 15;
|
||||
}
|
||||
if (level_prefix >= 16) {
|
||||
level_code += (1 << (level_prefix - 3)) - 4096;
|
||||
}
|
||||
|
||||
// Decode level from level_code
|
||||
int sign = level_code & 1;
|
||||
int abs_level = (level_code + 2) >> 1;
|
||||
int level = sign ? -abs_level : abs_level;
|
||||
|
||||
// NOTE: suffix_length update is done by the caller (read_block)
|
||||
// so it can use the adjusted abs_level after first-non-T1 correction.
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
// Read total_zeros by searching VLC table
|
||||
static int read_total_zeros_from_table(bs_t* b, const vlc_t* row, int max_zeros) {
|
||||
uint32_t lookahead = bs_next_bits(b, 16);
|
||||
for (int tz = 0; tz <= max_zeros; tz++) {
|
||||
if (row[tz].len == 0) continue;
|
||||
uint32_t code = lookahead >> (16 - row[tz].len);
|
||||
if (code == row[tz].code) {
|
||||
bs_skip_u(b, row[tz].len);
|
||||
return tz;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int read_total_zeros(bs_t* b, int total_coeff, int max_num_coeff) {
|
||||
if (max_num_coeff == 4) {
|
||||
// Chroma DC
|
||||
int max_zeros = 4 - total_coeff;
|
||||
return read_total_zeros_from_table(b, total_zeros_chroma_dc[total_coeff - 1], max_zeros);
|
||||
} else if (max_num_coeff == 15) {
|
||||
int max_zeros = 15 - total_coeff;
|
||||
return read_total_zeros_from_table(b, total_zeros_table_15[total_coeff - 1], max_zeros);
|
||||
} else {
|
||||
int max_zeros = 16 - total_coeff;
|
||||
return read_total_zeros_from_table(b, total_zeros_table[total_coeff - 1], max_zeros);
|
||||
}
|
||||
}
|
||||
|
||||
// Read run_before by searching VLC table
|
||||
static int read_run_before(bs_t* b, int zeros_left) {
|
||||
int idx = (zeros_left > 7) ? 6 : zeros_left - 1;
|
||||
int max_run = (zeros_left > 14) ? 14 : zeros_left;
|
||||
uint32_t lookahead = bs_next_bits(b, 16);
|
||||
|
||||
for (int rb = 0; rb <= max_run; rb++) {
|
||||
const vlc_t* e = &run_before_table[idx][rb];
|
||||
if (e->len == 0) continue;
|
||||
uint32_t code = lookahead >> (16 - e->len);
|
||||
if (code == e->code) {
|
||||
bs_skip_u(b, e->len);
|
||||
return rb;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int read_block(bs_t* b, int16_t* coeffs, int nc, int max_num_coeff) {
|
||||
memset(coeffs, 0, max_num_coeff * sizeof(int16_t));
|
||||
|
||||
// Step 1: Read coeff_token
|
||||
int total_coeff = 0, trailing_ones = 0;
|
||||
if (read_coeff_token(b, nc, &total_coeff, &trailing_ones) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (total_coeff == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// levels[] in reverse scan order (same as writer: [0] = last nonzero, etc.)
|
||||
int16_t levels[16];
|
||||
|
||||
// Step 2: Read trailing ones signs
|
||||
for (int i = trailing_ones - 1; i >= 0; i--) {
|
||||
int sign = bs_read_u1(b);
|
||||
levels[i] = sign ? -1 : 1;
|
||||
}
|
||||
|
||||
// Step 3: Read remaining levels
|
||||
int suffix_length = (total_coeff > 10 && trailing_ones < 3) ? 1 : 0;
|
||||
for (int i = trailing_ones; i < total_coeff; i++) {
|
||||
int level = read_level(b, &suffix_length);
|
||||
|
||||
// First non-T1 level adjustment (inverse of writer)
|
||||
if (i == trailing_ones && trailing_ones < 3) {
|
||||
level = (level >= 0) ? level + 1 : level - 1;
|
||||
}
|
||||
|
||||
levels[i] = (int16_t)level;
|
||||
|
||||
// Update suffix_length using the final abs_level (after adjustment)
|
||||
int abs_level = (level < 0) ? -level : level;
|
||||
if (suffix_length == 0) {
|
||||
suffix_length = 1;
|
||||
}
|
||||
if (abs_level > (3 << (suffix_length - 1)) && suffix_length < 6) {
|
||||
suffix_length++;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Read total_zeros
|
||||
int total_zeros = 0;
|
||||
if (total_coeff < max_num_coeff) {
|
||||
total_zeros = read_total_zeros(b, total_coeff, max_num_coeff);
|
||||
if (total_zeros < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Read run_before for each coefficient and place in output
|
||||
int zeros_left = total_zeros;
|
||||
int runs[16] = {0};
|
||||
for (int i = 0; i < total_coeff - 1; i++) {
|
||||
if (zeros_left > 0) {
|
||||
runs[i] = read_run_before(b, zeros_left);
|
||||
if (runs[i] < 0) {
|
||||
return -1;
|
||||
}
|
||||
zeros_left -= runs[i];
|
||||
}
|
||||
}
|
||||
// Last coefficient gets remaining zeros
|
||||
runs[total_coeff - 1] = zeros_left;
|
||||
|
||||
// Step 6: Place coefficients in zig-zag order
|
||||
// levels[0] is the highest-frequency nonzero coeff, levels[total_coeff-1] is lowest
|
||||
// runs[i] is the number of zeros between levels[i] and levels[i+1] (going downward)
|
||||
// Start at the highest occupied position and work down
|
||||
int pos = total_coeff + total_zeros - 1;
|
||||
|
||||
for (int i = 0; i < total_coeff; i++) {
|
||||
if (pos < 0 || pos >= max_num_coeff) {
|
||||
return -1;
|
||||
}
|
||||
coeffs[pos] = levels[i];
|
||||
pos--;
|
||||
pos -= runs[i];
|
||||
}
|
||||
|
||||
return total_coeff;
|
||||
}
|
||||
|
||||
int calc_nc(int nc_left, int nc_above) {
|
||||
if (nc_left < 0 && nc_above < 0) return 0;
|
||||
if (nc_left < 0) return nc_above;
|
||||
if (nc_above < 0) return nc_left;
|
||||
return (nc_left + nc_above + 1) >> 1;
|
||||
}
|
||||
|
||||
int write_block(bs_t* b, const int16_t* coeffs, int nc, int max_num_coeff) {
|
||||
// Step 1: Scan coefficients to find non-zeros
|
||||
// Coefficients are in zig-zag order. We need to:
|
||||
// - Count TotalCoeff (total non-zero coefficients)
|
||||
// - Count TrailingOnes (up to 3 consecutive +/-1 at the end)
|
||||
// - Build arrays of levels[] and runs[] for encoding
|
||||
|
||||
int16_t levels[16]; // Non-zero coefficient values (reverse order)
|
||||
int total_coeff = 0;
|
||||
int trailing_ones = 0;
|
||||
|
||||
// Scan from end to start (high frequency to low frequency)
|
||||
int last_nz = -1;
|
||||
for (int i = max_num_coeff - 1; i >= 0; i--) {
|
||||
if (coeffs[i] != 0) {
|
||||
if (last_nz < 0) last_nz = i;
|
||||
levels[total_coeff] = coeffs[i];
|
||||
total_coeff++;
|
||||
}
|
||||
}
|
||||
|
||||
if (total_coeff == 0) {
|
||||
// No coefficients - just write coeff_token(0, 0)
|
||||
write_coeff_token(b, 0, 0, nc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Count trailing ones (must be +/-1, up to 3 consecutive from the end)
|
||||
for (int i = 0; i < total_coeff && i < 3; i++) {
|
||||
if (levels[i] == 1 || levels[i] == -1) {
|
||||
trailing_ones++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Write coeff_token
|
||||
write_coeff_token(b, total_coeff, trailing_ones, nc);
|
||||
|
||||
// Step 3: Write trailing ones signs (in reverse order)
|
||||
for (int i = trailing_ones - 1; i >= 0; i--) {
|
||||
bs_write_u1(b, levels[i] < 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Step 4: Write remaining levels
|
||||
// Per H.264 spec 9.2.2: Start with suffixLength=1 when TotalCoeff > 10 and T1 < 3
|
||||
int suffix_length = (total_coeff > 10 && trailing_ones < 3) ? 1 : 0;
|
||||
for (int i = trailing_ones; i < total_coeff; i++) {
|
||||
int original_level = levels[i];
|
||||
int level = original_level;
|
||||
|
||||
// First non-T1 level adjustment
|
||||
if (i == trailing_ones && trailing_ones < 3) {
|
||||
// Subtract 1 from magnitude (encoded level can't be +/-1)
|
||||
level = (level > 0) ? level - 1 : level + 1;
|
||||
}
|
||||
|
||||
write_level(b, level, &suffix_length);
|
||||
|
||||
// Update suffix_length using the ORIGINAL abs_level (before adjustment)
|
||||
int abs_level = (original_level < 0) ? -original_level : original_level;
|
||||
if (suffix_length == 0) {
|
||||
suffix_length = 1;
|
||||
}
|
||||
if (abs_level > (3 << (suffix_length - 1)) && suffix_length < 6) {
|
||||
suffix_length++;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Write total_zeros (if TotalCoeff < max_num_coeff)
|
||||
if (total_coeff < max_num_coeff) {
|
||||
int total_zeros = last_nz + 1 - total_coeff;
|
||||
write_total_zeros(b, total_zeros, total_coeff, max_num_coeff);
|
||||
}
|
||||
|
||||
// Step 6: Compute and write run_before values
|
||||
// Need to re-scan to get the actual runs
|
||||
int zeros_left = last_nz + 1 - total_coeff;
|
||||
int coeff_idx = 0;
|
||||
for (int i = max_num_coeff - 1; i >= 0 && coeff_idx < total_coeff - 1; i--) {
|
||||
if (coeffs[i] != 0) {
|
||||
// Count zeros before this coefficient
|
||||
int run = 0;
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
if (coeffs[j] == 0) run++;
|
||||
else break;
|
||||
}
|
||||
if (zeros_left > 0) {
|
||||
write_run_before(b, run, zeros_left);
|
||||
zeros_left -= run;
|
||||
}
|
||||
coeff_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
return total_coeff;
|
||||
}
|
||||
|
||||
int copy_tail(bs_t* src, bs_t* dst, int tc, int t1, int max_num_coeff) {
|
||||
if (tc == 0) return 0;
|
||||
|
||||
/* 1. Copy trailing_ones sign bits */
|
||||
for (int i = 0; i < t1; i++) {
|
||||
bs_write_u1(dst, bs_read_u1(src));
|
||||
}
|
||||
|
||||
/* 2. Parse and copy level codes (must track suffix_length) */
|
||||
int suffix_length = (tc > 10 && t1 < 3) ? 1 : 0;
|
||||
for (int i = t1; i < tc; i++) {
|
||||
/* Read and copy level_prefix (unary) */
|
||||
int level_prefix = 0;
|
||||
while (bs_read_u1(src) == 0) {
|
||||
bs_write_u1(dst, 0);
|
||||
level_prefix++;
|
||||
}
|
||||
bs_write_u1(dst, 1);
|
||||
|
||||
/* Determine suffix size (Table 9-6) */
|
||||
int level_suffix_size;
|
||||
if (level_prefix == 14 && suffix_length == 0) {
|
||||
level_suffix_size = 4;
|
||||
} else if (level_prefix >= 15) {
|
||||
level_suffix_size = level_prefix - 3;
|
||||
} else {
|
||||
level_suffix_size = suffix_length;
|
||||
}
|
||||
|
||||
/* Copy suffix bits */
|
||||
int level_suffix = 0;
|
||||
if (level_suffix_size > 0) {
|
||||
level_suffix = (int)bs_read_u(src, level_suffix_size);
|
||||
bs_write_u(dst, level_suffix_size, (uint32_t)level_suffix);
|
||||
}
|
||||
|
||||
/* Reconstruct level_code for suffix_length update */
|
||||
int capped_prefix = (level_prefix < 15) ? level_prefix : 15;
|
||||
int level_code = (capped_prefix << suffix_length) + level_suffix;
|
||||
if (level_prefix >= 15 && suffix_length == 0) {
|
||||
level_code += 15;
|
||||
}
|
||||
if (level_prefix >= 16) {
|
||||
level_code += (1 << (level_prefix - 3)) - 4096;
|
||||
}
|
||||
|
||||
/* Decode abs_level */
|
||||
int abs_level = (level_code + 2) >> 1;
|
||||
|
||||
/* First non-trailing-one adjustment */
|
||||
if (i == t1 && t1 < 3) {
|
||||
abs_level += 1;
|
||||
}
|
||||
|
||||
/* Update suffix_length */
|
||||
if (suffix_length == 0) suffix_length = 1;
|
||||
if (abs_level > (3 << (suffix_length - 1)) && suffix_length < 6) {
|
||||
suffix_length++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. Decode and re-encode total_zeros (same VLC tables, bit-identical) */
|
||||
int total_zeros = 0;
|
||||
if (tc < max_num_coeff) {
|
||||
total_zeros = read_total_zeros(src, tc, max_num_coeff);
|
||||
if (total_zeros < 0) return -1;
|
||||
write_total_zeros(dst, total_zeros, tc, max_num_coeff);
|
||||
}
|
||||
|
||||
/* 4. Decode and re-encode run_before */
|
||||
int zeros_left = total_zeros;
|
||||
for (int i = 0; i < tc - 1 && zeros_left > 0; i++) {
|
||||
int run = read_run_before(src, zeros_left);
|
||||
if (run < 0) return -1;
|
||||
write_run_before(dst, run, zeros_left);
|
||||
zeros_left -= run;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace subcodec::cavlc
|
||||
19
third-party/subcodec/src/cavlc.h
vendored
Normal file
19
third-party/subcodec/src/cavlc.h
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "bs.h"
|
||||
|
||||
namespace subcodec::cavlc {
|
||||
|
||||
void write_coeff_token(bs_t* b, int total_coeff, int trailing_ones, int nc);
|
||||
void write_level_prefix(bs_t* b, int level_prefix);
|
||||
void write_total_zeros(bs_t* b, int total_zeros, int total_coeff, int max_num_coeff);
|
||||
void write_run_before(bs_t* b, int run_before, int zeros_left);
|
||||
int write_block(bs_t* b, const int16_t* coeffs, int nc, int max_num_coeff);
|
||||
int read_block(bs_t* b, int16_t* coeffs, int nc, int max_num_coeff);
|
||||
void write_level(bs_t* b, int level, int* suffix_length);
|
||||
int calc_nc(int nc_left, int nc_above);
|
||||
int read_coeff_token(bs_t* b, int nc, int* total_coeff, int* trailing_ones);
|
||||
int copy_tail(bs_t* src, bs_t* dst, int tc, int t1, int max_num_coeff);
|
||||
|
||||
} // namespace subcodec::cavlc
|
||||
15
third-party/subcodec/src/error.h
vendored
Normal file
15
third-party/subcodec/src/error.h
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <expected>
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
enum class Error {
|
||||
OUT_OF_SPACE,
|
||||
PARSE_ERROR,
|
||||
INVALID_INPUT,
|
||||
IO_ERROR,
|
||||
ENCODE_ERROR,
|
||||
};
|
||||
|
||||
} // namespace subcodec
|
||||
713
third-party/subcodec/src/frame_writer.cpp
vendored
Normal file
713
third-party/subcodec/src/frame_writer.cpp
vendored
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
#include "frame_writer.h"
|
||||
#include "types.h"
|
||||
#include "cavlc.h"
|
||||
#include "h264_stream.h"
|
||||
#include "bs.h"
|
||||
#include "tables.h"
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace subcodec::frame_writer {
|
||||
|
||||
// H.264 4x4 block index <-> (x4, y4) conversions
|
||||
// Block layout in a macroblock:
|
||||
// 0 1 4 5
|
||||
// 2 3 6 7
|
||||
// 8 9 12 13
|
||||
// 10 11 14 15
|
||||
static inline int blk_to_x4(int blk_idx) {
|
||||
return (blk_idx & 1) | ((blk_idx >> 1) & 2);
|
||||
}
|
||||
static inline int blk_to_y4(int blk_idx) {
|
||||
return ((blk_idx >> 1) & 1) | ((blk_idx >> 2) & 2);
|
||||
}
|
||||
static inline int xy4_to_blk(int x4, int y4) {
|
||||
return (x4 & 1) | ((y4 & 1) << 1) | ((x4 & 2) << 1) | ((y4 & 2) << 2);
|
||||
}
|
||||
|
||||
// Calculate nC for a luma 4x4 block using neighbor context
|
||||
static int calc_nc(int blk_idx, const MbContext* out_ctx,
|
||||
const MbContext* left, const MbContext* above) {
|
||||
int nc_left = -1, nc_above = -1;
|
||||
int x4 = blk_to_x4(blk_idx);
|
||||
int y4 = blk_to_y4(blk_idx);
|
||||
|
||||
if (x4 > 0) {
|
||||
nc_left = out_ctx->nc[xy4_to_blk(x4 - 1, y4)];
|
||||
} else if (left) {
|
||||
nc_left = left->nc[xy4_to_blk(3, y4)];
|
||||
}
|
||||
|
||||
if (y4 > 0) {
|
||||
nc_above = out_ctx->nc[xy4_to_blk(x4, y4 - 1)];
|
||||
} else if (above) {
|
||||
nc_above = above->nc[xy4_to_blk(x4, 3)];
|
||||
}
|
||||
|
||||
return subcodec::cavlc::calc_nc(nc_left, nc_above);
|
||||
}
|
||||
|
||||
// Calculate nC for a chroma 2x2 block using neighbor context
|
||||
// Chroma 2x2 layout: [0 1 / 2 3] — same as h264_parse.c calc_chroma_nc
|
||||
static int calc_chroma_nc(int blk_idx, const int* chroma_nc,
|
||||
const int* left_nc, const int* above_nc) {
|
||||
int cx = blk_idx % 2, cy = blk_idx / 2;
|
||||
int nc_left = -1, nc_above = -1;
|
||||
|
||||
if (cx > 0) nc_left = chroma_nc[blk_idx - 1];
|
||||
else if (left_nc) nc_left = left_nc[cy * 2 + 1];
|
||||
|
||||
if (cy > 0) nc_above = chroma_nc[blk_idx - 2];
|
||||
else if (above_nc) nc_above = above_nc[cx + 2];
|
||||
|
||||
return subcodec::cavlc::calc_nc(nc_left, nc_above);
|
||||
}
|
||||
|
||||
// Table 9-4(b): Coded block pattern mapping for Inter prediction
|
||||
using subcodec::tables::cbp_to_code_inter;
|
||||
using subcodec::tables::luma_block_order;
|
||||
using subcodec::tables::block_to_8x8;
|
||||
|
||||
// Write a NAL unit with start code to buffer
|
||||
// Returns bytes written
|
||||
static size_t write_nal_with_start_code(uint8_t* buf, size_t buf_size,
|
||||
h264_stream_t* h) {
|
||||
if (buf_size < 5) return 0;
|
||||
|
||||
// Start code
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
buf[2] = 0x00;
|
||||
buf[3] = 0x01;
|
||||
|
||||
// Write NAL unit to a temp buffer
|
||||
// h264bitstream's write_nal_unit has a quirk: it prepends an extra 0x00 byte
|
||||
// So we write to buf+3, then the actual NAL starts at buf+4
|
||||
uint8_t temp[1024];
|
||||
int nal_size = write_nal_unit(h, temp, (int)sizeof(temp));
|
||||
if (nal_size < 1) return 0;
|
||||
|
||||
// Skip the spurious leading 0x00 byte from write_nal_unit
|
||||
// Copy the rest starting at position 1
|
||||
if ((size_t)(nal_size - 1) > buf_size - 4) return 0;
|
||||
memcpy(buf + 4, temp + 1, (size_t)(nal_size - 1));
|
||||
|
||||
return 4 + (size_t)(nal_size - 1);
|
||||
}
|
||||
|
||||
size_t write_headers(std::span<uint8_t> output, const FrameParams& params) {
|
||||
h264_stream_t* h = h264_new();
|
||||
if (!h) return 0;
|
||||
|
||||
uint8_t* buf = output.data();
|
||||
size_t buf_size = output.size();
|
||||
size_t offset = 0;
|
||||
|
||||
// SPS
|
||||
h->nal->nal_ref_idc = 3;
|
||||
h->nal->nal_unit_type = NAL_UNIT_TYPE_SPS;
|
||||
|
||||
sps_t* sps = h->sps;
|
||||
|
||||
/* Pick the lowest level that supports this frame size (MB count).
|
||||
* Baseline Profile supports up to Level 5.2 (36,864 MBs).
|
||||
* High Profile is needed for Level 6.0+ (up to 139,264 MBs). */
|
||||
int total_mbs = params.width_mbs * params.height_mbs;
|
||||
bool need_high = (total_mbs > 36864);
|
||||
|
||||
sps->profile_idc = need_high ? 100 : 66; // High or Baseline
|
||||
sps->constraint_set0_flag = need_high ? 0 : 1;
|
||||
sps->constraint_set1_flag = need_high ? 0 : 1;
|
||||
sps->constraint_set2_flag = 0;
|
||||
sps->constraint_set3_flag = 0;
|
||||
sps->constraint_set4_flag = 0;
|
||||
sps->constraint_set5_flag = 0;
|
||||
|
||||
if (need_high) {
|
||||
/* High Profile SPS extensions — all at simplest/default values.
|
||||
* We still use CAVLC, no 8x8 transform, no scaling matrices. */
|
||||
sps->chroma_format_idc = 1; // 4:2:0
|
||||
sps->bit_depth_luma_minus8 = 0;
|
||||
sps->bit_depth_chroma_minus8 = 0;
|
||||
sps->qpprime_y_zero_transform_bypass_flag = 0;
|
||||
sps->seq_scaling_matrix_present_flag = 0;
|
||||
}
|
||||
|
||||
int level;
|
||||
if (total_mbs <= 1620) level = 30; // Level 3.0
|
||||
else if (total_mbs <= 3600) level = 31; // Level 3.1
|
||||
else if (total_mbs <= 5120) level = 32; // Level 3.2
|
||||
else if (total_mbs <= 8192) level = 40; // Level 4.0
|
||||
else if (total_mbs <= 8704) level = 42; // Level 4.2
|
||||
else if (total_mbs <= 22080) level = 50; // Level 5.0
|
||||
else if (total_mbs <= 36864) level = 51; // Level 5.1
|
||||
else level = 60; // Level 6.0 (139,264 MBs)
|
||||
sps->level_idc = level;
|
||||
sps->seq_parameter_set_id = 0;
|
||||
int log2mfn = (params.log2_max_frame_num > 4) ? params.log2_max_frame_num : 4;
|
||||
sps->log2_max_frame_num_minus4 = log2mfn - 4;
|
||||
sps->pic_order_cnt_type = 2; // No POC, infer from frame_num
|
||||
sps->num_ref_frames = 1;
|
||||
sps->gaps_in_frame_num_value_allowed_flag = 0;
|
||||
sps->pic_width_in_mbs_minus1 = params.width_mbs - 1;
|
||||
sps->pic_height_in_map_units_minus1 = params.height_mbs - 1;
|
||||
sps->frame_mbs_only_flag = 1;
|
||||
sps->direct_8x8_inference_flag = 1;
|
||||
sps->frame_cropping_flag = 0;
|
||||
sps->vui_parameters_present_flag = 0;
|
||||
|
||||
size_t sps_size = write_nal_with_start_code(buf + offset, buf_size - offset, h);
|
||||
if (sps_size == 0) { h264_free(h); return 0; }
|
||||
offset += sps_size;
|
||||
|
||||
// PPS
|
||||
h->nal->nal_ref_idc = 3;
|
||||
h->nal->nal_unit_type = NAL_UNIT_TYPE_PPS;
|
||||
|
||||
pps_t* pps = h->pps;
|
||||
pps->pic_parameter_set_id = 0;
|
||||
pps->seq_parameter_set_id = 0;
|
||||
pps->entropy_coding_mode_flag = 0; // CAVLC
|
||||
pps->pic_order_present_flag = 0;
|
||||
pps->num_slice_groups_minus1 = 0;
|
||||
pps->num_ref_idx_l0_active_minus1 = 0;
|
||||
pps->num_ref_idx_l1_active_minus1 = 0;
|
||||
pps->weighted_pred_flag = 0;
|
||||
pps->weighted_bipred_idc = 0;
|
||||
int qp = (params.qp > 0) ? params.qp : 26;
|
||||
pps->pic_init_qp_minus26 = qp - 26;
|
||||
pps->pic_init_qs_minus26 = 0;
|
||||
pps->chroma_qp_index_offset = 0;
|
||||
pps->deblocking_filter_control_present_flag = 1;
|
||||
pps->constrained_intra_pred_flag = 0;
|
||||
pps->redundant_pic_cnt_present_flag = 0;
|
||||
|
||||
size_t pps_size = write_nal_with_start_code(buf + offset, buf_size - offset, h);
|
||||
if (pps_size == 0) { h264_free(h); return 0; }
|
||||
offset += pps_size;
|
||||
|
||||
h264_free(h);
|
||||
return offset;
|
||||
}
|
||||
|
||||
// Return median of three int16_t values
|
||||
int16_t median3(int16_t a, int16_t b, int16_t c) {
|
||||
if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
|
||||
if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Predict motion vector using median of neighbors
|
||||
// Unavailable neighbors are treated as (0, 0)
|
||||
void predict_mv(const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right, int16_t* mvp) {
|
||||
int16_t mv_a[2] = {0, 0}; // left
|
||||
int16_t mv_b[2] = {0, 0}; // above
|
||||
int16_t mv_c[2] = {0, 0}; // above-right
|
||||
|
||||
if (left) {
|
||||
mv_a[0] = left->mv[0];
|
||||
mv_a[1] = left->mv[1];
|
||||
}
|
||||
if (above) {
|
||||
mv_b[0] = above->mv[0];
|
||||
mv_b[1] = above->mv[1];
|
||||
}
|
||||
if (above_right) {
|
||||
mv_c[0] = above_right->mv[0];
|
||||
mv_c[1] = above_right->mv[1];
|
||||
}
|
||||
|
||||
mvp[0] = median3(mv_a[0], mv_b[0], mv_c[0]);
|
||||
mvp[1] = median3(mv_a[1], mv_b[1], mv_c[1]);
|
||||
}
|
||||
|
||||
// Write P_16x16 macroblock to bitstream
|
||||
void write_mb_p16x16(bs_t* b, const MacroblockData& mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right,
|
||||
MbContext& out_ctx) {
|
||||
// 1. mb_type = 0 for P_L0_16x16 in P-slice (ref_idx_l0=0 implied for single ref)
|
||||
bs_write_ue(b, 0);
|
||||
|
||||
// 2. Motion vector delta (predicted MV subtracted)
|
||||
int16_t mvp[2];
|
||||
predict_mv(left, above, above_right, mvp);
|
||||
bs_write_se(b, mb.mv_x - mvp[0]);
|
||||
bs_write_se(b, mb.mv_y - mvp[1]);
|
||||
|
||||
// 3. coded_block_pattern (always written per H.264 spec for P_L0_16x16)
|
||||
int cbp = (mb.cbp_chroma << 4) | mb.cbp_luma;
|
||||
|
||||
// Write CBP using mapped exp-golomb code (Table 9-4(b))
|
||||
bs_write_ue(b, cbp_to_code_inter[cbp]);
|
||||
|
||||
if (cbp != 0) {
|
||||
// mb_qp_delta = 0 (no QP change)
|
||||
bs_write_se(b, 0);
|
||||
|
||||
// 4. Write residual with CAVLC
|
||||
// For P_16x16, residual structure is:
|
||||
// - 16 luma 4x4 blocks (in raster scan of 8x8 blocks, then 4x4 within)
|
||||
// - Chroma DC Cb, Chroma DC Cr (if cbp_chroma >= 1)
|
||||
// - 4 Chroma AC Cb blocks, 4 Chroma AC Cr blocks (if cbp_chroma == 2)
|
||||
|
||||
// Initialize nC tracking for output context
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out_ctx.nc[i] = 0;
|
||||
}
|
||||
|
||||
// Luma residual: 4 8x8 blocks, each containing 4 4x4 blocks
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int parent_8x8 = block_to_8x8[blk_idx];
|
||||
|
||||
// Check if this 8x8 block has coefficients
|
||||
if (!(mb.cbp_luma & (1 << parent_8x8))) {
|
||||
out_ctx.nc[blk_idx] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int nc = calc_nc(blk_idx, &out_ctx, left, above);
|
||||
|
||||
// Write the 4x4 block coefficients
|
||||
int16_t coeffs[16];
|
||||
coeffs[0] = mb.luma_dc[blk_idx]; // Use luma_dc for the DC coefficient
|
||||
for (int j = 0; j < 15; j++) {
|
||||
coeffs[j + 1] = mb.luma_ac[blk_idx][j];
|
||||
}
|
||||
|
||||
int tc = subcodec::cavlc::write_block(b, coeffs, nc, 16);
|
||||
out_ctx.nc[blk_idx] = tc;
|
||||
}
|
||||
|
||||
// Chroma DC (if cbp_chroma >= 1)
|
||||
if (mb.cbp_chroma >= 1) {
|
||||
// Cb DC (4 coefficients for 4:2:0)
|
||||
subcodec::cavlc::write_block(b, mb.cb_dc, -1, 4);
|
||||
// Cr DC (4 coefficients for 4:2:0)
|
||||
subcodec::cavlc::write_block(b, mb.cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
// Chroma AC (if cbp_chroma == 2)
|
||||
if (mb.cbp_chroma == 2) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = calc_chroma_nc(i, out_ctx.nc_cb,
|
||||
left ? left->nc_cb : NULL,
|
||||
above ? above->nc_cb : NULL);
|
||||
out_ctx.nc_cb[i] = subcodec::cavlc::write_block(b, mb.cb_ac[i], nc, 15);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = calc_chroma_nc(i, out_ctx.nc_cr,
|
||||
left ? left->nc_cr : NULL,
|
||||
above ? above->nc_cr : NULL);
|
||||
out_ctx.nc_cr[i] = subcodec::cavlc::write_block(b, mb.cr_ac[i], nc, 15);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No residual - just initialize context
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out_ctx.nc[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Update output context with this macroblock's MV
|
||||
out_ctx.mv[0] = mb.mv_x;
|
||||
out_ctx.mv[1] = mb.mv_y;
|
||||
}
|
||||
|
||||
// Compute I_16x16 mb_type in P-slice
|
||||
// I_16x16 mb_type in P-slice: 6 + mode + 4*cbp_chroma + 12*cbp_dc
|
||||
// mode: 0=Vertical, 1=Horizontal, 2=DC, 3=Plane
|
||||
// cbp_chroma: 0, 1, or 2
|
||||
// ac_has_nonzero: 0 or 1 (whether any of the 16 AC blocks have non-zero coeffs)
|
||||
static int i16x16_mb_type_p_slice(int pred_mode, int cbp_chroma, int ac_has_nonzero) {
|
||||
return 6 + pred_mode + 4 * cbp_chroma + 12 * ac_has_nonzero;
|
||||
}
|
||||
|
||||
// Write I_16x16 macroblock to bitstream
|
||||
void write_mb_i16x16(bs_t* b, const MacroblockData& mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
MbContext& out_ctx) {
|
||||
// 1. Determine if any AC block has non-zero coefficients
|
||||
int ac_has_nonzero = 0;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
for (int j = 0; j < 15; j++) {
|
||||
if (mb.luma_ac[i][j] != 0) {
|
||||
ac_has_nonzero = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ac_has_nonzero) break;
|
||||
}
|
||||
|
||||
// 2. Write mb_type (I_16x16 in P-slice)
|
||||
int mb_type = i16x16_mb_type_p_slice(static_cast<int>(mb.intra_pred_mode), mb.cbp_chroma, ac_has_nonzero);
|
||||
bs_write_ue(b, (uint32_t)mb_type);
|
||||
|
||||
// 3. Write intra_chroma_pred_mode
|
||||
bs_write_ue(b, static_cast<uint32_t>(mb.intra_chroma_mode));
|
||||
|
||||
// 4. Write mb_qp_delta = 0
|
||||
bs_write_se(b, 0);
|
||||
|
||||
// 5. Write Luma DC block (nC from block 0 neighbors, max_num_coeff = 16)
|
||||
{
|
||||
int dc_nc = calc_nc(0, &out_ctx, left, above);
|
||||
subcodec::cavlc::write_block(b, mb.luma_dc, dc_nc, 16);
|
||||
}
|
||||
|
||||
// Initialize nC tracking for output context
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out_ctx.nc[i] = 0;
|
||||
}
|
||||
|
||||
// 6. Write 16 Luma AC blocks (max_num_coeff = 15, DC is separate)
|
||||
if (ac_has_nonzero) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int nc = calc_nc(blk_idx, &out_ctx, left, above);
|
||||
// Write the AC block (15 coefficients, no DC)
|
||||
out_ctx.nc[blk_idx] = subcodec::cavlc::write_block(b, mb.luma_ac[blk_idx], nc, 15);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Write chroma DC blocks (nC = -1 for chroma DC, max_num_coeff = 4)
|
||||
// Cb DC then Cr DC
|
||||
if (mb.cbp_chroma > 0) {
|
||||
subcodec::cavlc::write_block(b, mb.cb_dc, -1, 4);
|
||||
subcodec::cavlc::write_block(b, mb.cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
// 8. Write chroma AC blocks if cbp_chroma == 2
|
||||
if (mb.cbp_chroma == 2) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = calc_chroma_nc(i, out_ctx.nc_cb,
|
||||
left ? left->nc_cb : NULL,
|
||||
above ? above->nc_cb : NULL);
|
||||
out_ctx.nc_cb[i] = subcodec::cavlc::write_block(b, mb.cb_ac[i], nc, 15);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = calc_chroma_nc(i, out_ctx.nc_cr,
|
||||
left ? left->nc_cr : NULL,
|
||||
above ? above->nc_cr : NULL);
|
||||
out_ctx.nc_cr[i] = subcodec::cavlc::write_block(b, mb.cr_ac[i], nc, 15);
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Update context - I-macroblock has no motion vector
|
||||
out_ctx.mv[0] = 0;
|
||||
out_ctx.mv[1] = 0;
|
||||
}
|
||||
|
||||
std::expected<size_t, Error> write_p_frame_ex(
|
||||
std::span<uint8_t> output,
|
||||
const FrameParams& params,
|
||||
const MacroblockData* mbs,
|
||||
int frame_num) {
|
||||
|
||||
uint8_t* buf = output.data();
|
||||
size_t buf_size = output.size();
|
||||
|
||||
if (buf_size < 8) return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
// NAL header
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
buf[2] = 0x00;
|
||||
buf[3] = 0x01;
|
||||
buf[4] = (2 << 5) | 1; // nal_ref_idc=2, nal_unit_type=1 (non-IDR slice)
|
||||
|
||||
uint8_t rbsp[256 * 1024];
|
||||
bs_t b;
|
||||
bs_init(&b, rbsp, sizeof(rbsp));
|
||||
|
||||
// Slice header (same as write_p_frame)
|
||||
bs_write_ue(&b, 0); // first_mb_in_slice = 0
|
||||
bs_write_ue(&b, 5); // slice_type = 5 (P, all macroblocks)
|
||||
bs_write_ue(&b, 0); // pic_parameter_set_id = 0
|
||||
{
|
||||
int l2mfn = (params.log2_max_frame_num > 4) ? params.log2_max_frame_num : 4;
|
||||
int max_fn = 1 << l2mfn;
|
||||
bs_write_u(&b, l2mfn, frame_num % max_fn);
|
||||
}
|
||||
|
||||
// num_ref_idx_active_override_flag
|
||||
bs_write_u(&b, 1, 0);
|
||||
|
||||
// ref_pic_list_modification - no modifications
|
||||
bs_write_u(&b, 1, 0); // ref_pic_list_modification_flag_l0
|
||||
|
||||
// dec_ref_pic_marking - adaptive_ref_pic_marking_mode_flag
|
||||
bs_write_u(&b, 1, 0);
|
||||
|
||||
// slice_qp_delta
|
||||
bs_write_se(&b, params.slice_qp_delta);
|
||||
|
||||
// deblocking_filter_control_present_flag = 1, so:
|
||||
bs_write_ue(&b, 1); // disable_deblocking_filter_idc = 1 (disabled)
|
||||
|
||||
// Allocate context for neighbor tracking
|
||||
int num_mbs = params.width_mbs * params.height_mbs;
|
||||
std::vector<MbContext> ctx_row_vec(params.width_mbs);
|
||||
MbContext* ctx_row = ctx_row_vec.data();
|
||||
MbContext ctx_left = {};
|
||||
|
||||
int mb_idx = 0;
|
||||
int prev_was_skip = 0; // Track if previous iteration was a skip run
|
||||
while (mb_idx < num_mbs) {
|
||||
const MacroblockData& mb = mbs[mb_idx];
|
||||
int mb_x = mb_idx % params.width_mbs;
|
||||
int mb_y = mb_idx / params.width_mbs;
|
||||
|
||||
// Get neighbor contexts
|
||||
MbContext* above = (mb_y > 0) ? &ctx_row[mb_x] : NULL;
|
||||
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
|
||||
MbContext* above_right = (mb_y > 0 && mb_x < params.width_mbs - 1)
|
||||
? &ctx_row[mb_x + 1] : NULL;
|
||||
|
||||
// Handle skip run
|
||||
if (mb.mb_type == MbType::SKIP) {
|
||||
int skip_count = 1;
|
||||
while (mb_idx + skip_count < num_mbs &&
|
||||
mbs[mb_idx + skip_count].mb_type == MbType::SKIP) {
|
||||
skip_count++;
|
||||
}
|
||||
bs_write_ue(&b, (uint32_t)skip_count);
|
||||
|
||||
// Update context for skipped MBs (MV=0, nC=0)
|
||||
for (int i = 0; i < skip_count; i++) {
|
||||
int idx = mb_idx + i;
|
||||
int x = idx % params.width_mbs;
|
||||
MbContext skip_ctx = {};
|
||||
ctx_left = skip_ctx;
|
||||
ctx_row[x] = skip_ctx;
|
||||
}
|
||||
mb_idx += skip_count;
|
||||
prev_was_skip = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-skip: write mb_skip_run = 0 only if we didn't just write a skip run
|
||||
if (!prev_was_skip) {
|
||||
bs_write_ue(&b, 0);
|
||||
}
|
||||
prev_was_skip = 0;
|
||||
|
||||
// Write macroblock based on type
|
||||
MbContext out_ctx = {};
|
||||
switch (mb.mb_type) {
|
||||
case MbType::P_16x16:
|
||||
write_mb_p16x16(&b, mb, left_ctx, above, above_right, out_ctx);
|
||||
break;
|
||||
case MbType::I_16x16:
|
||||
write_mb_i16x16(&b, mb, left_ctx, above, out_ctx);
|
||||
break;
|
||||
default:
|
||||
// Unknown type, treat as skip (should not happen)
|
||||
out_ctx = MbContext{};
|
||||
break;
|
||||
}
|
||||
|
||||
// Update context
|
||||
ctx_left = out_ctx;
|
||||
ctx_row[mb_x] = out_ctx;
|
||||
mb_idx++;
|
||||
}
|
||||
|
||||
// RBSP trailing bits
|
||||
write_rbsp_trailing_bits(&b);
|
||||
|
||||
size_t rbsp_size = (size_t)bs_pos(&b);
|
||||
|
||||
// Copy RBSP to output with emulation prevention
|
||||
size_t out_pos = 5;
|
||||
int zero_count = 0;
|
||||
for (size_t i = 0; i < rbsp_size && out_pos < buf_size; i++) {
|
||||
uint8_t byte = rbsp[i];
|
||||
if (zero_count >= 2 && byte <= 3) {
|
||||
if (out_pos >= buf_size) {
|
||||
return std::unexpected(Error::OUT_OF_SPACE);
|
||||
}
|
||||
buf[out_pos++] = 0x03;
|
||||
zero_count = 0;
|
||||
}
|
||||
if (out_pos >= buf_size) {
|
||||
return std::unexpected(Error::OUT_OF_SPACE);
|
||||
}
|
||||
buf[out_pos++] = byte;
|
||||
zero_count = (byte == 0) ? zero_count + 1 : 0;
|
||||
}
|
||||
|
||||
return out_pos;
|
||||
}
|
||||
|
||||
// Compute I_16x16 mb_type in I-slice
|
||||
// I_16x16 mb_type in I-slice: 1 + pred_mode + 4*cbp_chroma + 12*ac_has_nonzero
|
||||
// (offset is 1, not 6 as in P-slice)
|
||||
static int i16x16_mb_type_i_slice(int pred_mode, int cbp_chroma, int ac_has_nonzero) {
|
||||
return 1 + pred_mode + 4 * cbp_chroma + 12 * ac_has_nonzero;
|
||||
}
|
||||
|
||||
std::expected<size_t, Error> write_idr_frame_ex(
|
||||
std::span<uint8_t> output,
|
||||
const FrameParams& params,
|
||||
const MacroblockData* mbs) {
|
||||
|
||||
uint8_t* buf = output.data();
|
||||
size_t buf_size = output.size();
|
||||
|
||||
if (buf_size < 8) return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
// NAL header: nal_ref_idc=3, nal_unit_type=5 (IDR slice)
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
buf[2] = 0x00;
|
||||
buf[3] = 0x01;
|
||||
buf[4] = (3 << 5) | 5;
|
||||
|
||||
uint8_t rbsp[256 * 1024];
|
||||
bs_t b;
|
||||
bs_init(&b, rbsp, sizeof(rbsp));
|
||||
|
||||
// Slice header for IDR I-slice
|
||||
bs_write_ue(&b, 0); // first_mb_in_slice = 0
|
||||
bs_write_ue(&b, 7); // slice_type = 7 (I, all macroblocks)
|
||||
bs_write_ue(&b, 0); // pic_parameter_set_id = 0
|
||||
{
|
||||
int l2mfn = (params.log2_max_frame_num > 4) ? params.log2_max_frame_num : 4;
|
||||
bs_write_u(&b, l2mfn, 0); // frame_num = 0
|
||||
}
|
||||
bs_write_ue(&b, 0); // idr_pic_id = 0
|
||||
|
||||
// dec_ref_pic_marking for IDR with nal_ref_idc > 0
|
||||
bs_write_u(&b, 1, 0); // no_output_of_prior_pics_flag = 0
|
||||
bs_write_u(&b, 1, 0); // long_term_reference_flag = 0
|
||||
|
||||
bs_write_se(&b, params.slice_qp_delta); // slice_qp_delta
|
||||
|
||||
// deblocking_filter_control_present_flag = 1, so:
|
||||
bs_write_ue(&b, 1); // disable_deblocking_filter_idc = 1 (disabled)
|
||||
|
||||
// Allocate context for neighbor tracking
|
||||
int num_mbs = params.width_mbs * params.height_mbs;
|
||||
std::vector<MbContext> ctx_row_vec(params.width_mbs);
|
||||
MbContext* ctx_row = ctx_row_vec.data();
|
||||
MbContext ctx_left = {};
|
||||
|
||||
for (int mb_idx = 0; mb_idx < num_mbs; mb_idx++) {
|
||||
const MacroblockData& mb = mbs[mb_idx];
|
||||
int mb_x = mb_idx % params.width_mbs;
|
||||
int mb_y = mb_idx / params.width_mbs;
|
||||
|
||||
// Get neighbor contexts
|
||||
MbContext* above = (mb_y > 0) ? &ctx_row[mb_x] : NULL;
|
||||
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
|
||||
|
||||
MbContext out_ctx = {};
|
||||
|
||||
switch (mb.mb_type) {
|
||||
case MbType::I_16x16: {
|
||||
// Determine if any AC block has non-zero coefficients
|
||||
int ac_has_nonzero = 0;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
for (int j = 0; j < 15; j++) {
|
||||
if (mb.luma_ac[i][j] != 0) {
|
||||
ac_has_nonzero = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ac_has_nonzero) break;
|
||||
}
|
||||
|
||||
// Write mb_type (I_16x16 in I-slice, offset 1)
|
||||
int mbt = i16x16_mb_type_i_slice(static_cast<int>(mb.intra_pred_mode), mb.cbp_chroma, ac_has_nonzero);
|
||||
bs_write_ue(&b, (uint32_t)mbt);
|
||||
|
||||
// Write intra_chroma_pred_mode
|
||||
bs_write_ue(&b, static_cast<uint32_t>(mb.intra_chroma_mode));
|
||||
|
||||
// Write mb_qp_delta = 0
|
||||
bs_write_se(&b, 0);
|
||||
|
||||
// Write Luma DC block (nC from block 0 neighbors, max_num_coeff = 16)
|
||||
{
|
||||
int dc_nc = calc_nc(0, &out_ctx, left_ctx, above);
|
||||
subcodec::cavlc::write_block(&b, mb.luma_dc, dc_nc, 16);
|
||||
}
|
||||
|
||||
// Initialize nC tracking
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out_ctx.nc[i] = 0;
|
||||
}
|
||||
|
||||
// Write 16 Luma AC blocks if ac_has_nonzero
|
||||
if (ac_has_nonzero) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int nc = calc_nc(blk_idx, &out_ctx, left_ctx, above);
|
||||
out_ctx.nc[blk_idx] = subcodec::cavlc::write_block(&b, mb.luma_ac[blk_idx], nc, 15);
|
||||
}
|
||||
}
|
||||
|
||||
// Write chroma DC blocks
|
||||
if (mb.cbp_chroma > 0) {
|
||||
subcodec::cavlc::write_block(&b, mb.cb_dc, -1, 4);
|
||||
subcodec::cavlc::write_block(&b, mb.cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
// Write chroma AC blocks if cbp_chroma == 2
|
||||
if (mb.cbp_chroma == 2) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
subcodec::cavlc::write_block(&b, mb.cb_ac[i], 0, 15);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
subcodec::cavlc::write_block(&b, mb.cr_ac[i], 0, 15);
|
||||
}
|
||||
}
|
||||
|
||||
// I-macroblock has no motion vector
|
||||
out_ctx.mv[0] = 0;
|
||||
out_ctx.mv[1] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown type - treat as empty (should not happen in IDR)
|
||||
out_ctx = MbContext{};
|
||||
break;
|
||||
}
|
||||
|
||||
// Update context
|
||||
ctx_left = out_ctx;
|
||||
ctx_row[mb_x] = out_ctx;
|
||||
}
|
||||
|
||||
// RBSP trailing bits
|
||||
write_rbsp_trailing_bits(&b);
|
||||
|
||||
size_t rbsp_size = (size_t)bs_pos(&b);
|
||||
|
||||
// Copy RBSP to output with emulation prevention
|
||||
size_t out_pos = 5;
|
||||
int zero_count = 0;
|
||||
for (size_t i = 0; i < rbsp_size && out_pos < buf_size; i++) {
|
||||
uint8_t byte = rbsp[i];
|
||||
if (zero_count >= 2 && byte <= 3) {
|
||||
if (out_pos >= buf_size) {
|
||||
return std::unexpected(Error::OUT_OF_SPACE);
|
||||
}
|
||||
buf[out_pos++] = 0x03;
|
||||
zero_count = 0;
|
||||
}
|
||||
if (out_pos >= buf_size) {
|
||||
return std::unexpected(Error::OUT_OF_SPACE);
|
||||
}
|
||||
buf[out_pos++] = byte;
|
||||
zero_count = (byte == 0) ? zero_count + 1 : 0;
|
||||
}
|
||||
|
||||
return out_pos;
|
||||
}
|
||||
|
||||
} // namespace subcodec::frame_writer
|
||||
40
third-party/subcodec/src/frame_writer.h
vendored
Normal file
40
third-party/subcodec/src/frame_writer.h
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <expected>
|
||||
#include "types.h"
|
||||
#include "error.h"
|
||||
#include "bs.h"
|
||||
|
||||
namespace subcodec::frame_writer {
|
||||
|
||||
size_t write_headers(std::span<uint8_t> output, const FrameParams& params);
|
||||
|
||||
int16_t median3(int16_t a, int16_t b, int16_t c);
|
||||
|
||||
void predict_mv(const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right, int16_t* mvp);
|
||||
|
||||
void write_mb_p16x16(bs_t* b, const MacroblockData& mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right,
|
||||
MbContext& out_ctx);
|
||||
|
||||
void write_mb_i16x16(bs_t* b, const MacroblockData& mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
MbContext& out_ctx);
|
||||
|
||||
std::expected<size_t, Error> write_p_frame_ex(
|
||||
std::span<uint8_t> output,
|
||||
const FrameParams& params,
|
||||
const MacroblockData* mbs,
|
||||
int frame_num);
|
||||
|
||||
std::expected<size_t, Error> write_idr_frame_ex(
|
||||
std::span<uint8_t> output,
|
||||
const FrameParams& params,
|
||||
const MacroblockData* mbs);
|
||||
|
||||
} // namespace subcodec::frame_writer
|
||||
684
third-party/subcodec/src/h264_parser.cpp
vendored
Normal file
684
third-party/subcodec/src/h264_parser.cpp
vendored
Normal file
|
|
@ -0,0 +1,684 @@
|
|||
#include "h264_parser.h"
|
||||
#include "cavlc.h"
|
||||
#include "bs.h"
|
||||
#include "tables.h"
|
||||
#include "frame_writer.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
using namespace subcodec;
|
||||
using subcodec::tables::cbp_to_code_inter;
|
||||
using subcodec::tables::cbp_to_code_intra;
|
||||
using subcodec::tables::luma_block_order;
|
||||
using subcodec::tables::block_to_8x8;
|
||||
|
||||
namespace {
|
||||
|
||||
// Inverse of cbp_to_code_inter: given codeNum, find cbp_luma and cbp_chroma
|
||||
static int decode_cbp_inter(uint32_t code_num, uint8_t* cbp_luma, uint8_t* cbp_chroma) {
|
||||
for (int i = 0; i < 48; i++) {
|
||||
if (cbp_to_code_inter[i] == code_num) {
|
||||
*cbp_chroma = (uint8_t)(i / 16);
|
||||
*cbp_luma = (uint8_t)(i % 16);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Inverse of cbp_to_code_intra: given codeNum, find cbp_luma and cbp_chroma
|
||||
static int decode_cbp_intra(uint32_t code_num, uint8_t* cbp_luma, uint8_t* cbp_chroma) {
|
||||
for (int i = 0; i < 48; i++) {
|
||||
if (cbp_to_code_intra[i] == code_num) {
|
||||
*cbp_chroma = (uint8_t)(i / 16);
|
||||
*cbp_luma = (uint8_t)(i % 16);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Remove emulation prevention bytes (00 00 03 -> 00 00)
|
||||
static size_t remove_emulation_prevention(const uint8_t* src, size_t src_len,
|
||||
uint8_t* dst, size_t dst_size) {
|
||||
size_t si = 0, di = 0;
|
||||
while (si < src_len && di < dst_size) {
|
||||
if (si + 2 < src_len && src[si] == 0x00 && src[si + 1] == 0x00 && src[si + 2] == 0x03) {
|
||||
dst[di++] = 0x00;
|
||||
if (di < dst_size) dst[di++] = 0x00;
|
||||
si += 3;
|
||||
} else {
|
||||
dst[di++] = src[si++];
|
||||
}
|
||||
}
|
||||
return di;
|
||||
}
|
||||
|
||||
// Check if there is more RBSP data (i.e., not just trailing bits)
|
||||
static int more_rbsp_data(bs_t* b) {
|
||||
// If we're at or past the end, no more data
|
||||
if (bs_eof(b)) return 0;
|
||||
|
||||
// Save position
|
||||
bs_t saved;
|
||||
bs_clone(&saved, b);
|
||||
|
||||
// Find the last 1 bit in the remaining data
|
||||
// If all remaining bits are 0 after the current position, no data
|
||||
// If the remaining bits are exactly: 1 followed by 0s to byte boundary, no data
|
||||
|
||||
// Simple approach: check if we have more than 8 bits remaining
|
||||
// (trailing bits are at most 8 bits)
|
||||
int bits_remaining = 0;
|
||||
uint8_t* p = b->p;
|
||||
int bl = b->bits_left;
|
||||
|
||||
// Count remaining bits
|
||||
bits_remaining = bl; // bits left in current byte
|
||||
if (p < b->end) {
|
||||
bits_remaining += (int)(b->end - p - 1) * 8;
|
||||
}
|
||||
|
||||
if (bits_remaining <= 0) return 0;
|
||||
if (bits_remaining > 8) return 1; // More than trailing bits
|
||||
|
||||
// 8 or fewer bits remaining - check if it's just trailing bits
|
||||
// Trailing bits: 1 followed by 0s
|
||||
// Read remaining bits and check
|
||||
uint32_t remaining = 0;
|
||||
for (int i = 0; i < bits_remaining; i++) {
|
||||
remaining = (remaining << 1) | bs_read_u1(&saved);
|
||||
}
|
||||
|
||||
// Check if remaining == 1 << (bits_remaining - 1) (just a stop bit + zeros)
|
||||
// Or more generally: remaining should be a power of 2
|
||||
if (remaining != 0 && (remaining & (remaining - 1)) == 0) {
|
||||
return 0; // It's just trailing bits
|
||||
}
|
||||
|
||||
return 1; // There's real data
|
||||
}
|
||||
|
||||
// H.264 4x4 block index <-> (x4, y4) conversions
|
||||
// Block layout in a macroblock:
|
||||
// 0 1 4 5
|
||||
// 2 3 6 7
|
||||
// 8 9 12 13
|
||||
// 10 11 14 15
|
||||
static inline int blk_to_x4(int blk_idx) {
|
||||
return (blk_idx & 1) | ((blk_idx >> 1) & 2);
|
||||
}
|
||||
static inline int blk_to_y4(int blk_idx) {
|
||||
return ((blk_idx >> 1) & 1) | ((blk_idx >> 2) & 2);
|
||||
}
|
||||
static inline int xy4_to_blk(int x4, int y4) {
|
||||
return (x4 & 1) | ((y4 & 1) << 1) | ((x4 & 2) << 1) | ((y4 & 2) << 2);
|
||||
}
|
||||
|
||||
// Calculate nC for a luma 4x4 block using neighbor context
|
||||
static int calc_block_nc(int blk_idx, const MbContext* out_ctx,
|
||||
const MbContext* left, const MbContext* above) {
|
||||
int nc_left = -1, nc_above = -1;
|
||||
int x4 = blk_to_x4(blk_idx);
|
||||
int y4 = blk_to_y4(blk_idx);
|
||||
|
||||
// Left neighbor
|
||||
if (x4 > 0) {
|
||||
nc_left = out_ctx->nc[xy4_to_blk(x4 - 1, y4)];
|
||||
} else if (left) {
|
||||
nc_left = left->nc[xy4_to_blk(3, y4)];
|
||||
}
|
||||
|
||||
// Above neighbor
|
||||
if (y4 > 0) {
|
||||
nc_above = out_ctx->nc[xy4_to_blk(x4, y4 - 1)];
|
||||
} else if (above) {
|
||||
nc_above = above->nc[xy4_to_blk(x4, 3)];
|
||||
}
|
||||
|
||||
return subcodec::cavlc::calc_nc(nc_left, nc_above);
|
||||
}
|
||||
|
||||
// Calculate nC for a chroma 4x4 block (Cb or Cr)
|
||||
static int calc_chroma_nc(int blk_idx, const int* chroma_nc,
|
||||
const int* left_nc, const int* above_nc) {
|
||||
int cx = blk_idx % 2, cy = blk_idx / 2;
|
||||
int nc_left = -1, nc_above = -1;
|
||||
|
||||
// Chroma 2x2 block layout: [0 1 / 2 3]
|
||||
if (cx > 0) nc_left = chroma_nc[blk_idx - 1];
|
||||
else if (left_nc) nc_left = left_nc[cy * 2 + 1]; // left MB's right column at same y
|
||||
|
||||
if (cy > 0) nc_above = chroma_nc[blk_idx - 2];
|
||||
else if (above_nc) nc_above = above_nc[cx + 2]; // above MB's bottom row at same x
|
||||
|
||||
return subcodec::cavlc::calc_nc(nc_left, nc_above);
|
||||
}
|
||||
|
||||
// Read chroma AC blocks and update context.
|
||||
static void read_chroma_ac(bs_t* b, MacroblockData* mb,
|
||||
MbContext* out_ctx,
|
||||
const MbContext* left,
|
||||
const MbContext* above) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = calc_chroma_nc(i, out_ctx->nc_cb,
|
||||
left ? left->nc_cb : NULL,
|
||||
above ? above->nc_cb : NULL);
|
||||
int16_t coeffs[15];
|
||||
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 15);
|
||||
out_ctx->nc_cb[i] = tc;
|
||||
for (int j = 0; j < 15; j++)
|
||||
mb->cb_ac[i][j] = coeffs[j];
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = calc_chroma_nc(i, out_ctx->nc_cr,
|
||||
left ? left->nc_cr : NULL,
|
||||
above ? above->nc_cr : NULL);
|
||||
int16_t coeffs[15];
|
||||
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 15);
|
||||
out_ctx->nc_cr[i] = tc;
|
||||
for (int j = 0; j < 15; j++)
|
||||
mb->cr_ac[i][j] = coeffs[j];
|
||||
}
|
||||
}
|
||||
|
||||
// Read P_16x16 macroblock (inverse of write_mb_p16x16)
|
||||
static void read_mb_p16x16(bs_t* b, MacroblockData* mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right,
|
||||
MbContext* out_ctx) {
|
||||
mb->mb_type = MbType::P_16x16;
|
||||
|
||||
// Read MV delta and reconstruct MV
|
||||
int16_t mvp[2];
|
||||
subcodec::frame_writer::predict_mv(left, above, above_right, mvp);
|
||||
int32_t mvd_x = bs_read_se(b);
|
||||
int32_t mvd_y = bs_read_se(b);
|
||||
mb->mv_x = (int16_t)(mvp[0] + mvd_x);
|
||||
mb->mv_y = (int16_t)(mvp[1] + mvd_y);
|
||||
|
||||
// Read CBP (always present per H.264 spec for P_L0_16x16)
|
||||
uint32_t cbp_code = bs_read_ue(b);
|
||||
*out_ctx = MbContext{};
|
||||
|
||||
if (decode_cbp_inter(cbp_code, &mb->cbp_luma, &mb->cbp_chroma) != 0) {
|
||||
mb->cbp_luma = 0;
|
||||
mb->cbp_chroma = 0;
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb->cbp_luma == 0 && mb->cbp_chroma == 0) {
|
||||
// No residual
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
return;
|
||||
}
|
||||
|
||||
// Read mb_qp_delta (discarded)
|
||||
bs_read_se(b);
|
||||
|
||||
// Read luma residual
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int parent_8x8 = block_to_8x8[blk_idx];
|
||||
|
||||
if (!(mb->cbp_luma & (1 << parent_8x8))) {
|
||||
out_ctx->nc[blk_idx] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int nc = calc_block_nc(blk_idx, out_ctx, left, above);
|
||||
|
||||
int16_t coeffs[16];
|
||||
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 16);
|
||||
out_ctx->nc[blk_idx] = tc;
|
||||
|
||||
// Split: coeffs[0] -> luma_dc, coeffs[1..15] -> luma_ac
|
||||
mb->luma_dc[blk_idx] = coeffs[0];
|
||||
for (int j = 0; j < 15; j++) {
|
||||
mb->luma_ac[blk_idx][j] = coeffs[j + 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Chroma DC
|
||||
if (mb->cbp_chroma >= 1) {
|
||||
subcodec::cavlc::read_block(b, mb->cb_dc, -1, 4);
|
||||
subcodec::cavlc::read_block(b, mb->cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
// Chroma AC
|
||||
if (mb->cbp_chroma == 2) {
|
||||
read_chroma_ac(b, mb, out_ctx, left, above);
|
||||
}
|
||||
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
}
|
||||
|
||||
// Read P_8x8 or P_8x8ref0 macroblock (mb_type 3 or 4 in P-slice)
|
||||
// We store as P_16x16 with the first sub-partition's MV for simplicity
|
||||
static void read_mb_p8x8(bs_t* b, MacroblockData* mb, int is_ref0,
|
||||
const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right,
|
||||
MbContext* out_ctx) {
|
||||
mb->mb_type = MbType::P_16x16; // Approximate as P_16x16
|
||||
|
||||
// Read 4 sub_mb_type values
|
||||
int sub_mb_type[4];
|
||||
int num_sub_parts[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sub_mb_type[i] = (int)bs_read_ue(b);
|
||||
// P sub_mb_type: 0=8x8(1 part), 1=8x4(2), 2=4x8(2), 3=4x4(4)
|
||||
switch (sub_mb_type[i]) {
|
||||
case 0: num_sub_parts[i] = 1; break;
|
||||
case 1: num_sub_parts[i] = 2; break;
|
||||
case 2: num_sub_parts[i] = 2; break;
|
||||
case 3: num_sub_parts[i] = 4; break;
|
||||
default: num_sub_parts[i] = 1; break;
|
||||
}
|
||||
}
|
||||
|
||||
// ref_idx: for P_8x8 (not ref0), read ref_idx for each 8x8 partition
|
||||
if (!is_ref0) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
bs_read_ue(b); // ref_idx_l0[i] (typically 0 with single ref)
|
||||
}
|
||||
}
|
||||
|
||||
// MVD for each sub-partition
|
||||
int16_t first_mv_x = 0, first_mv_y = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < num_sub_parts[i]; j++) {
|
||||
int32_t mvd_x = bs_read_se(b);
|
||||
int32_t mvd_y = bs_read_se(b);
|
||||
if (i == 0 && j == 0) {
|
||||
// Use first partition's MV as representative
|
||||
int16_t mvp[2];
|
||||
subcodec::frame_writer::predict_mv(left, above, above_right, mvp);
|
||||
first_mv_x = (int16_t)(mvp[0] + mvd_x);
|
||||
first_mv_y = (int16_t)(mvp[1] + mvd_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mb->mv_x = first_mv_x;
|
||||
mb->mv_y = first_mv_y;
|
||||
|
||||
// CBP
|
||||
uint32_t cbp_code = bs_read_ue(b);
|
||||
*out_ctx = MbContext{};
|
||||
|
||||
if (decode_cbp_inter(cbp_code, &mb->cbp_luma, &mb->cbp_chroma) != 0) {
|
||||
mb->cbp_luma = 0;
|
||||
mb->cbp_chroma = 0;
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb->cbp_luma == 0 && mb->cbp_chroma == 0) {
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
return;
|
||||
}
|
||||
|
||||
// mb_qp_delta
|
||||
bs_read_se(b);
|
||||
|
||||
// Luma residual
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int parent_8x8 = block_to_8x8[blk_idx];
|
||||
|
||||
if (!(mb->cbp_luma & (1 << parent_8x8))) {
|
||||
out_ctx->nc[blk_idx] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int nc = calc_block_nc(blk_idx, out_ctx, left, above);
|
||||
int16_t coeffs[16];
|
||||
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 16);
|
||||
out_ctx->nc[blk_idx] = tc;
|
||||
mb->luma_dc[blk_idx] = coeffs[0];
|
||||
for (int j = 0; j < 15; j++)
|
||||
mb->luma_ac[blk_idx][j] = coeffs[j + 1];
|
||||
}
|
||||
|
||||
// Chroma DC
|
||||
if (mb->cbp_chroma >= 1) {
|
||||
subcodec::cavlc::read_block(b, mb->cb_dc, -1, 4);
|
||||
subcodec::cavlc::read_block(b, mb->cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
// Chroma AC
|
||||
if (mb->cbp_chroma == 2) {
|
||||
read_chroma_ac(b, mb, out_ctx, left, above);
|
||||
}
|
||||
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
}
|
||||
|
||||
// Read I_16x16 macroblock (inverse of write_mb_i16x16)
|
||||
// offset: mb_type_code - 6 for P-slice, mb_type_code - 1 for I-slice
|
||||
static void read_mb_i16x16(bs_t* b, MacroblockData* mb, int offset,
|
||||
const MbContext* left, const MbContext* above,
|
||||
MbContext* out_ctx) {
|
||||
mb->mb_type = MbType::I_16x16;
|
||||
|
||||
int ac_has_nonzero = offset / 12;
|
||||
int cbp_chroma = (offset % 12) / 4;
|
||||
int pred_mode = offset % 4;
|
||||
|
||||
mb->intra_pred_mode = static_cast<subcodec::I16PredMode>(pred_mode);
|
||||
mb->cbp_chroma = (uint8_t)cbp_chroma;
|
||||
|
||||
// Read intra_chroma_pred_mode
|
||||
mb->intra_chroma_mode = static_cast<subcodec::ChromaPredMode>(bs_read_ue(b));
|
||||
|
||||
// Read mb_qp_delta
|
||||
int32_t qpd = bs_read_se(b);
|
||||
*out_ctx = MbContext{};
|
||||
|
||||
// Read luma DC (16 coefficients) - nC from block 0 neighbors per H.264 spec
|
||||
int dc_nc = calc_block_nc(0, out_ctx, left, above);
|
||||
int dc_tc = subcodec::cavlc::read_block(b, mb->luma_dc, dc_nc, 16);
|
||||
|
||||
// Read luma AC blocks if present
|
||||
if (ac_has_nonzero) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int nc = calc_block_nc(blk_idx, out_ctx, left, above);
|
||||
|
||||
int16_t coeffs[15];
|
||||
int tc = subcodec::cavlc::read_block(b, coeffs, nc, 15);
|
||||
out_ctx->nc[blk_idx] = tc;
|
||||
|
||||
for (int j = 0; j < 15; j++)
|
||||
mb->luma_ac[blk_idx][j] = coeffs[j];
|
||||
}
|
||||
}
|
||||
|
||||
// Chroma DC
|
||||
if (cbp_chroma > 0) {
|
||||
subcodec::cavlc::read_block(b, mb->cb_dc, -1, 4);
|
||||
subcodec::cavlc::read_block(b, mb->cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
// Chroma AC
|
||||
if (cbp_chroma == 2) {
|
||||
read_chroma_ac(b, mb, out_ctx, left, above);
|
||||
}
|
||||
|
||||
out_ctx->mv[0] = 0;
|
||||
out_ctx->mv[1] = 0;
|
||||
}
|
||||
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
std::expected<std::vector<MacroblockData>, Error>
|
||||
H264Parser::parse_slice(std::span<const uint8_t> nal_data,
|
||||
const FrameParams& params) {
|
||||
return parse_slice_ex(nal_data, params, nullptr);
|
||||
}
|
||||
|
||||
std::expected<std::vector<MacroblockData>, Error>
|
||||
H264Parser::parse_slice_ex(std::span<const uint8_t> nal_data,
|
||||
const FrameParams& params,
|
||||
int* out_slice_qp_delta) {
|
||||
const uint8_t* buf = nal_data.data();
|
||||
size_t buf_size = nal_data.size();
|
||||
|
||||
if (!buf || buf_size < 5)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
// Verify start code
|
||||
if (buf[0] != 0x00 || buf[1] != 0x00 || buf[2] != 0x00 || buf[3] != 0x01)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
// Read NAL header
|
||||
uint8_t nal_header = buf[4];
|
||||
int nal_ref_idc = (nal_header >> 5) & 0x3;
|
||||
int nal_unit_type = nal_header & 0x1F;
|
||||
int is_idr = (nal_unit_type == 5);
|
||||
|
||||
// Remove emulation prevention bytes using member buffer
|
||||
rbsp_buf_.resize(buf_size);
|
||||
size_t rbsp_size = remove_emulation_prevention(buf + 5, buf_size - 5,
|
||||
rbsp_buf_.data(), rbsp_buf_.size());
|
||||
|
||||
bs_t b;
|
||||
bs_init(&b, rbsp_buf_.data(), rbsp_size);
|
||||
|
||||
// Slice header
|
||||
bs_read_ue(&b); // first_mb_in_slice
|
||||
uint32_t slice_type = bs_read_ue(&b); // slice_type
|
||||
bs_read_ue(&b); // pic_parameter_set_id
|
||||
int frame_num_bits = (params.log2_max_frame_num > 0) ? params.log2_max_frame_num : 4;
|
||||
bs_read_u(&b, frame_num_bits); // frame_num
|
||||
|
||||
int is_p_slice = (slice_type == 0 || slice_type == 5);
|
||||
int is_i_slice = (slice_type == 2 || slice_type == 7);
|
||||
|
||||
if (is_idr) {
|
||||
bs_read_ue(&b); // idr_pic_id
|
||||
}
|
||||
|
||||
// pic_order_cnt parsing (depends on SPS pic_order_cnt_type)
|
||||
if (params.log2_max_frame_num > 0) {
|
||||
int poc_type = params.pic_order_cnt_type;
|
||||
if (poc_type == 0) {
|
||||
int poc_lsb_bits = (params.log2_max_pic_order_cnt_lsb > 0)
|
||||
? params.log2_max_pic_order_cnt_lsb : 4;
|
||||
bs_read_u(&b, poc_lsb_bits); // pic_order_cnt_lsb
|
||||
} else if (poc_type == 1) {
|
||||
bs_read_se(&b); // delta_pic_order_cnt[0]
|
||||
}
|
||||
// poc_type == 2: nothing in slice header
|
||||
}
|
||||
|
||||
// P-slice header extras
|
||||
if (is_p_slice) {
|
||||
if (bs_read_u1(&b)) { // num_ref_idx_active_override_flag
|
||||
bs_read_ue(&b); // num_ref_idx_l0_active_minus1
|
||||
}
|
||||
if (bs_read_u1(&b)) { // ref_pic_list_modification_flag_l0
|
||||
uint32_t mod_op;
|
||||
do {
|
||||
mod_op = bs_read_ue(&b);
|
||||
if (mod_op != 3) {
|
||||
bs_read_ue(&b);
|
||||
}
|
||||
} while (mod_op != 3);
|
||||
}
|
||||
}
|
||||
|
||||
// dec_ref_pic_marking
|
||||
if (nal_ref_idc > 0) {
|
||||
if (is_idr) {
|
||||
bs_read_u1(&b); // no_output_of_prior_pics_flag
|
||||
bs_read_u1(&b); // long_term_reference_flag
|
||||
} else {
|
||||
bs_read_u1(&b); // adaptive_ref_pic_marking_mode_flag
|
||||
}
|
||||
}
|
||||
|
||||
int32_t slice_qp_delta = bs_read_se(&b); // slice_qp_delta
|
||||
if (out_slice_qp_delta) *out_slice_qp_delta = (int)slice_qp_delta;
|
||||
uint32_t disable_deblocking = bs_read_ue(&b); // disable_deblocking_filter_idc
|
||||
if (disable_deblocking != 1) {
|
||||
bs_read_se(&b); // slice_alpha_c0_offset_div2
|
||||
bs_read_se(&b); // slice_beta_offset_div2
|
||||
}
|
||||
|
||||
// Parse macroblocks
|
||||
int num_mbs = params.width_mbs * params.height_mbs;
|
||||
std::vector<MacroblockData> out_mbs(static_cast<size_t>(num_mbs));
|
||||
|
||||
ctx_row_.assign(static_cast<size_t>(params.width_mbs), MbContext{});
|
||||
MbContext ctx_left = {};
|
||||
|
||||
int mb_idx = 0;
|
||||
bool error = false;
|
||||
|
||||
if (is_p_slice) {
|
||||
while (mb_idx < num_mbs) {
|
||||
// Read mb_skip_run (always, per H.264 spec syntax)
|
||||
uint32_t skip_run = bs_read_ue(&b);
|
||||
|
||||
for (uint32_t i = 0; i < skip_run && mb_idx < num_mbs; i++) {
|
||||
int mb_x = mb_idx % params.width_mbs;
|
||||
out_mbs[mb_idx].mb_type = MbType::SKIP;
|
||||
MbContext skip_ctx = {};
|
||||
ctx_left = skip_ctx;
|
||||
ctx_row_[mb_x] = skip_ctx;
|
||||
mb_idx++;
|
||||
}
|
||||
|
||||
if (skip_run > 0 && !more_rbsp_data(&b)) {
|
||||
// Fill remaining MBs as skip
|
||||
while (mb_idx < num_mbs) {
|
||||
int mb_x2 = mb_idx % params.width_mbs;
|
||||
out_mbs[mb_idx].mb_type = MbType::SKIP;
|
||||
MbContext skip_ctx2 = {};
|
||||
ctx_left = skip_ctx2;
|
||||
ctx_row_[mb_x2] = skip_ctx2;
|
||||
mb_idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (mb_idx >= num_mbs) break;
|
||||
|
||||
int mb_x = mb_idx % params.width_mbs;
|
||||
int mb_y = mb_idx / params.width_mbs;
|
||||
MbContext* above = (mb_y > 0) ? &ctx_row_[mb_x] : NULL;
|
||||
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
|
||||
MbContext* above_right = (mb_y > 0 && mb_x < params.width_mbs - 1)
|
||||
? &ctx_row_[mb_x + 1] : NULL;
|
||||
|
||||
uint32_t mb_type_code = bs_read_ue(&b);
|
||||
MbContext out_ctx = {};
|
||||
|
||||
if (mb_type_code == 0) {
|
||||
read_mb_p16x16(&b, &out_mbs[mb_idx], left_ctx, above, above_right, &out_ctx);
|
||||
} else if (mb_type_code >= 1 && mb_type_code <= 2) {
|
||||
// P_L0_L0_16x8 or P_L0_L0_8x16 - approximate as P_16x16
|
||||
out_mbs[mb_idx].mb_type = MbType::P_16x16;
|
||||
// 2 partitions, each with MVD
|
||||
int16_t mvp[2];
|
||||
subcodec::frame_writer::predict_mv(left_ctx, above, above_right, mvp);
|
||||
int32_t mvd_x = bs_read_se(&b);
|
||||
int32_t mvd_y = bs_read_se(&b);
|
||||
out_mbs[mb_idx].mv_x = (int16_t)(mvp[0] + mvd_x);
|
||||
out_mbs[mb_idx].mv_y = (int16_t)(mvp[1] + mvd_y);
|
||||
// Second partition MVD
|
||||
bs_read_se(&b); // mvd_x
|
||||
bs_read_se(&b); // mvd_y
|
||||
|
||||
// CBP + residual (same as P_16x16)
|
||||
uint32_t cbp_code = bs_read_ue(&b);
|
||||
out_ctx = MbContext{};
|
||||
if (decode_cbp_inter(cbp_code, &out_mbs[mb_idx].cbp_luma, &out_mbs[mb_idx].cbp_chroma) == 0 &&
|
||||
(out_mbs[mb_idx].cbp_luma != 0 || out_mbs[mb_idx].cbp_chroma != 0)) {
|
||||
bs_read_se(&b); // qp_delta
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int parent_8x8 = block_to_8x8[blk_idx];
|
||||
if (!(out_mbs[mb_idx].cbp_luma & (1 << parent_8x8))) {
|
||||
out_ctx.nc[blk_idx] = 0; continue;
|
||||
}
|
||||
int nc = calc_block_nc(blk_idx, &out_ctx, left_ctx, above);
|
||||
int16_t coeffs[16];
|
||||
out_ctx.nc[blk_idx] = subcodec::cavlc::read_block(&b, coeffs, nc, 16);
|
||||
}
|
||||
if (out_mbs[mb_idx].cbp_chroma >= 1) {
|
||||
int16_t dummy4[4];
|
||||
subcodec::cavlc::read_block(&b, dummy4, -1, 4);
|
||||
subcodec::cavlc::read_block(&b, dummy4, -1, 4);
|
||||
}
|
||||
if (out_mbs[mb_idx].cbp_chroma == 2) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int16_t dummy15[15];
|
||||
subcodec::cavlc::read_block(&b, dummy15, 0, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
out_ctx.mv[0] = out_mbs[mb_idx].mv_x;
|
||||
out_ctx.mv[1] = out_mbs[mb_idx].mv_y;
|
||||
} else if (mb_type_code == 3) {
|
||||
read_mb_p8x8(&b, &out_mbs[mb_idx], 0, left_ctx, above, above_right, &out_ctx);
|
||||
} else if (mb_type_code == 4) {
|
||||
read_mb_p8x8(&b, &out_mbs[mb_idx], 1, left_ctx, above, above_right, &out_ctx);
|
||||
} else if (mb_type_code >= 6 && mb_type_code <= 29) {
|
||||
int offset = (int)mb_type_code - 6;
|
||||
read_mb_i16x16(&b, &out_mbs[mb_idx], offset, left_ctx, above, &out_ctx);
|
||||
} else {
|
||||
fprintf(stderr, "h264_parse: P-slice bad mb_type=%u at mb_idx=%d/%d\n",
|
||||
mb_type_code, mb_idx, num_mbs);
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
|
||||
ctx_left = out_ctx;
|
||||
ctx_row_[mb_x] = out_ctx;
|
||||
mb_idx++;
|
||||
|
||||
if (!more_rbsp_data(&b)) {
|
||||
// Remaining MBs are skip (end of slice data)
|
||||
while (mb_idx < num_mbs) {
|
||||
int mb_x2 = mb_idx % params.width_mbs;
|
||||
out_mbs[mb_idx].mb_type = MbType::SKIP;
|
||||
MbContext skip_ctx2 = {};
|
||||
ctx_left = skip_ctx2;
|
||||
ctx_row_[mb_x2] = skip_ctx2;
|
||||
mb_idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (is_i_slice) {
|
||||
for (mb_idx = 0; mb_idx < num_mbs; mb_idx++) {
|
||||
int mb_x = mb_idx % params.width_mbs;
|
||||
int mb_y = mb_idx / params.width_mbs;
|
||||
MbContext* above = (mb_y > 0) ? &ctx_row_[mb_x] : NULL;
|
||||
MbContext* left_ctx = (mb_x > 0) ? &ctx_left : NULL;
|
||||
|
||||
if (bs_eof(&b)) {
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t mb_type_code = bs_read_ue(&b);
|
||||
MbContext out_ctx = {};
|
||||
|
||||
if (mb_type_code >= 1 && mb_type_code <= 24) {
|
||||
int offset = (int)mb_type_code - 1;
|
||||
read_mb_i16x16(&b, &out_mbs[mb_idx], offset, left_ctx, above, &out_ctx);
|
||||
} else {
|
||||
fprintf(stderr, "h264_parse: I-slice bad mb_type=%u at mb_idx=%d/%d\n",
|
||||
mb_type_code, mb_idx, num_mbs);
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
|
||||
ctx_left = out_ctx;
|
||||
ctx_row_[mb_x] = out_ctx;
|
||||
}
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return std::unexpected(Error::PARSE_ERROR);
|
||||
}
|
||||
|
||||
return out_mbs;
|
||||
}
|
||||
|
||||
} // namespace subcodec
|
||||
29
third-party/subcodec/src/h264_parser.h
vendored
Normal file
29
third-party/subcodec/src/h264_parser.h
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
#include "types.h"
|
||||
#include "error.h"
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
class H264Parser {
|
||||
public:
|
||||
std::expected<std::vector<MacroblockData>, Error> parse_slice(
|
||||
std::span<const uint8_t> nal_data,
|
||||
const FrameParams& params);
|
||||
|
||||
std::expected<std::vector<MacroblockData>, Error> parse_slice_ex(
|
||||
std::span<const uint8_t> nal_data,
|
||||
const FrameParams& params,
|
||||
int* out_slice_qp_delta = nullptr);
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> rbsp_buf_;
|
||||
std::vector<MbContext> ctx_row_;
|
||||
};
|
||||
|
||||
} // namespace subcodec
|
||||
627
third-party/subcodec/src/mbs_encode.cpp
vendored
Normal file
627
third-party/subcodec/src/mbs_encode.cpp
vendored
Normal file
|
|
@ -0,0 +1,627 @@
|
|||
#include "mbs_encode.h"
|
||||
#include "cavlc.h"
|
||||
#include "bs.h"
|
||||
#include "tables.h"
|
||||
#include "frame_writer.h"
|
||||
#include "mbs_mux_common.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace subcodec::mbs {
|
||||
|
||||
using subcodec::tables::cbp_to_code_inter;
|
||||
using subcodec::tables::luma_block_order;
|
||||
using subcodec::tables::block_to_8x8;
|
||||
using subcodec::frame_writer::predict_mv;
|
||||
|
||||
/* ---- nC computation (same algorithm as mbs_mux_common) ---- */
|
||||
|
||||
static int blk_to_x4(int blk_idx) {
|
||||
return (blk_idx & 1) | ((blk_idx >> 1) & 2);
|
||||
}
|
||||
|
||||
static int blk_to_y4(int blk_idx) {
|
||||
return ((blk_idx >> 1) & 1) | ((blk_idx >> 2) & 2);
|
||||
}
|
||||
|
||||
static int xy4_to_blk(int x4, int y4) {
|
||||
return (x4 & 1) | ((y4 & 1) << 1) | ((x4 & 2) << 1) | ((y4 & 2) << 2);
|
||||
}
|
||||
|
||||
static int enc_calc_nc(int nc_left, int nc_above) {
|
||||
if (nc_left >= 0 && nc_above >= 0) return (nc_left + nc_above + 1) >> 1;
|
||||
if (nc_left >= 0) return nc_left;
|
||||
if (nc_above >= 0) return nc_above;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int enc_calc_nc_luma(int blk_idx, const MbContext* cur,
|
||||
const MbContext* left, const MbContext* above) {
|
||||
int nc_left = -1, nc_above = -1;
|
||||
int x4 = blk_to_x4(blk_idx);
|
||||
int y4 = blk_to_y4(blk_idx);
|
||||
|
||||
if (x4 > 0) {
|
||||
nc_left = cur->nc[xy4_to_blk(x4 - 1, y4)];
|
||||
} else if (left) {
|
||||
nc_left = left->nc[xy4_to_blk(3, y4)];
|
||||
}
|
||||
|
||||
if (y4 > 0) {
|
||||
nc_above = cur->nc[xy4_to_blk(x4, y4 - 1)];
|
||||
} else if (above) {
|
||||
nc_above = above->nc[xy4_to_blk(x4, 3)];
|
||||
}
|
||||
|
||||
return enc_calc_nc(nc_left, nc_above);
|
||||
}
|
||||
|
||||
static int enc_calc_nc_chroma(int blk_idx, const int* cur_nc,
|
||||
const int* left_nc, const int* above_nc) {
|
||||
int cx = blk_idx % 2, cy = blk_idx / 2;
|
||||
int nc_left = -1, nc_above = -1;
|
||||
|
||||
if (cx > 0) nc_left = cur_nc[blk_idx - 1];
|
||||
else if (left_nc) nc_left = left_nc[cy * 2 + 1];
|
||||
|
||||
if (cy > 0) nc_above = cur_nc[blk_idx - 2];
|
||||
else if (above_nc) nc_above = above_nc[cx + 2];
|
||||
|
||||
return enc_calc_nc(nc_left, nc_above);
|
||||
}
|
||||
|
||||
/* ---- Per-MB bitstream encoding into a temporary buffer ---- */
|
||||
|
||||
/* Compute bit count from a bs_t state */
|
||||
static int bs_bit_count(bs_t* b) {
|
||||
int byte_count = (int)(b->p - b->start);
|
||||
int partial = (b->bits_left < 8) ? (8 - b->bits_left) : 0;
|
||||
return byte_count * 8 + partial;
|
||||
}
|
||||
|
||||
/* Byte-align a bs_t and return total bytes written */
|
||||
static int bs_byte_align(bs_t* b) {
|
||||
if (b->bits_left != 8) {
|
||||
b->p++;
|
||||
b->bits_left = 8;
|
||||
}
|
||||
return (int)(b->p - b->start);
|
||||
}
|
||||
|
||||
/* Encode P_16x16 MB bitstream (exp-golomb header + CAVLC blocks) into bs_t.
|
||||
* Returns 0 on success, -1 on error. Populates out_ctx. */
|
||||
static int encode_mb_p16x16_bs(bs_t* b, const MacroblockData* mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
const MbContext* above_right,
|
||||
MbContext* out_ctx) {
|
||||
/* Header blob: ue(0) + se(mvd_x) + se(mvd_y) + ue(cbp_code) [+ se(qp_delta)] */
|
||||
bs_write_ue(b, 0); /* mb_type = P_L0_16x16 */
|
||||
|
||||
int16_t mvp[2];
|
||||
predict_mv(left, above, above_right, mvp);
|
||||
bs_write_se(b, mb->mv_x - mvp[0]);
|
||||
bs_write_se(b, mb->mv_y - mvp[1]);
|
||||
|
||||
int cbp = (mb->cbp_chroma << 4) | mb->cbp_luma;
|
||||
bs_write_ue(b, cbp_to_code_inter[cbp]);
|
||||
|
||||
if (cbp != 0) {
|
||||
bs_write_se(b, 0); /* qp_delta */
|
||||
}
|
||||
|
||||
/* Initialize output context */
|
||||
*out_ctx = MbContext{};
|
||||
|
||||
if (cbp != 0) {
|
||||
/* Luma blocks — real nC from neighbor context */
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int parent_8x8 = block_to_8x8[blk_idx];
|
||||
|
||||
if (!(mb->cbp_luma & (1 << parent_8x8))) {
|
||||
out_ctx->nc[blk_idx] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int16_t coeffs[16];
|
||||
coeffs[0] = mb->luma_dc[blk_idx];
|
||||
for (int j = 0; j < 15; j++) {
|
||||
coeffs[j + 1] = mb->luma_ac[blk_idx][j];
|
||||
}
|
||||
|
||||
int nc = enc_calc_nc_luma(blk_idx, out_ctx, left, above);
|
||||
int tc = subcodec::cavlc::write_block(b, coeffs, nc, 16);
|
||||
out_ctx->nc[blk_idx] = tc;
|
||||
}
|
||||
|
||||
/* Chroma DC — canonical nC=-1 */
|
||||
if (mb->cbp_chroma >= 1) {
|
||||
subcodec::cavlc::write_block(b, mb->cb_dc, -1, 4);
|
||||
subcodec::cavlc::write_block(b, mb->cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
/* Chroma AC — real nC from neighbor context */
|
||||
if (mb->cbp_chroma == 2) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cb,
|
||||
left ? left->nc_cb : nullptr,
|
||||
above ? above->nc_cb : nullptr);
|
||||
int tc = subcodec::cavlc::write_block(b, mb->cb_ac[i], nc, 15);
|
||||
out_ctx->nc_cb[i] = tc;
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cr,
|
||||
left ? left->nc_cr : nullptr,
|
||||
above ? above->nc_cr : nullptr);
|
||||
int tc = subcodec::cavlc::write_block(b, mb->cr_ac[i], nc, 15);
|
||||
out_ctx->nc_cr[i] = tc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Update MV in context */
|
||||
out_ctx->mv[0] = mb->mv_x;
|
||||
out_ctx->mv[1] = mb->mv_y;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Encode I_16x16 MB bitstream (exp-golomb header + CAVLC blocks) into bs_t.
|
||||
* Returns 0 on success, -1 on error. Populates out_ctx. */
|
||||
static int encode_mb_i16x16_bs(bs_t* b, const MacroblockData* mb,
|
||||
const MbContext* left, const MbContext* above,
|
||||
MbContext* out_ctx) {
|
||||
/* Determine if any AC block has non-zero coefficients */
|
||||
int ac_has_nonzero = 0;
|
||||
for (int i = 0; i < 16 && !ac_has_nonzero; i++) {
|
||||
for (int j = 0; j < 15; j++) {
|
||||
if (mb->luma_ac[i][j] != 0) {
|
||||
ac_has_nonzero = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Header blob */
|
||||
int mb_type = 6 + static_cast<int>(mb->intra_pred_mode) + 4 * mb->cbp_chroma + 12 * ac_has_nonzero;
|
||||
bs_write_ue(b, (uint32_t)mb_type);
|
||||
bs_write_ue(b, static_cast<uint32_t>(mb->intra_chroma_mode));
|
||||
bs_write_se(b, 0); /* qp_delta */
|
||||
|
||||
/* Initialize output context */
|
||||
*out_ctx = MbContext{};
|
||||
|
||||
/* Luma DC: real nC */
|
||||
{
|
||||
int nc = enc_calc_nc_luma(0, out_ctx, left, above);
|
||||
subcodec::cavlc::write_block(b, mb->luma_dc, nc, 16);
|
||||
}
|
||||
|
||||
/* Luma AC: real nC from neighbor context */
|
||||
if (ac_has_nonzero) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int blk_idx = luma_block_order[i];
|
||||
int nc = enc_calc_nc_luma(blk_idx, out_ctx, left, above);
|
||||
int tc = subcodec::cavlc::write_block(b, mb->luma_ac[blk_idx], nc, 15);
|
||||
out_ctx->nc[blk_idx] = tc;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chroma DC: canonical nC=-1 */
|
||||
if (mb->cbp_chroma >= 1) {
|
||||
subcodec::cavlc::write_block(b, mb->cb_dc, -1, 4);
|
||||
subcodec::cavlc::write_block(b, mb->cr_dc, -1, 4);
|
||||
}
|
||||
|
||||
/* Chroma AC: real nC from neighbor context */
|
||||
if (mb->cbp_chroma == 2) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cb,
|
||||
left ? left->nc_cb : nullptr,
|
||||
above ? above->nc_cb : nullptr);
|
||||
int tc = subcodec::cavlc::write_block(b, mb->cb_ac[i], nc, 15);
|
||||
out_ctx->nc_cb[i] = tc;
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nc = enc_calc_nc_chroma(i, out_ctx->nc_cr,
|
||||
left ? left->nc_cr : nullptr,
|
||||
above ? above->nc_cr : nullptr);
|
||||
int tc = subcodec::cavlc::write_block(b, mb->cr_ac[i], nc, 15);
|
||||
out_ctx->nc_cr[i] = tc;
|
||||
}
|
||||
}
|
||||
|
||||
out_ctx->mv[0] = 0;
|
||||
out_ctx->mv[1] = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- Row blob assembly ---- */
|
||||
|
||||
/* Per-MB encoded data (temporary, before row assembly) */
|
||||
struct MbEncoded {
|
||||
bool is_skip;
|
||||
int bit_count; /* bits in bitstream (0 for SKIP) */
|
||||
uint8_t buf[4096]; /* bitstream data (only valid if !is_skip) */
|
||||
};
|
||||
|
||||
/* Assemble row blob from per-MB encoded data.
|
||||
* Appends [leading_skips][trailing_skips][blob_bit_count LE][blob bytes...] to out.
|
||||
* For all-skip rows, appends just [leading_skips=width][trailing_skips=0][blob_bit_count=0]. */
|
||||
static void assemble_row_blob(const MbEncoded* mbs, int width,
|
||||
std::vector<uint8_t>& out) {
|
||||
/* Find first and last non-skip */
|
||||
int first_nonskip = -1, last_nonskip = -1;
|
||||
for (int i = 0; i < width; i++) {
|
||||
if (!mbs[i].is_skip) {
|
||||
if (first_nonskip < 0) first_nonskip = i;
|
||||
last_nonskip = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (first_nonskip < 0) {
|
||||
/* All-skip row */
|
||||
out.push_back(static_cast<uint8_t>(width)); /* leading_skips */
|
||||
out.push_back(0); /* trailing_skips */
|
||||
uint16_t zero = 0;
|
||||
out.push_back(static_cast<uint8_t>(zero & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((zero >> 8) & 0xFF));
|
||||
out.push_back(0); /* leading_zero_bits */
|
||||
out.push_back(0); /* trailing_zero_bits */
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t leading = static_cast<uint8_t>(first_nonskip);
|
||||
uint8_t trailing = static_cast<uint8_t>(width - 1 - last_nonskip);
|
||||
|
||||
/* Build the blob: non-skip MB bitstreams with interleaved ue(skip_count) */
|
||||
/* First, compute total bits needed */
|
||||
/* We need a temporary bs_t to write the blob */
|
||||
/* Max size: sum of all MB bitstreams + skip run exp-golomb codes */
|
||||
int total_mb_bits = 0;
|
||||
for (int i = first_nonskip; i <= last_nonskip; i++) {
|
||||
if (!mbs[i].is_skip) {
|
||||
total_mb_bits += mbs[i].bit_count;
|
||||
}
|
||||
}
|
||||
/* Skip run ue() codes: at most width * 32 bits each (generous) */
|
||||
int max_blob_bytes = (total_mb_bits + width * 32 + 7) / 8 + 16;
|
||||
|
||||
std::vector<uint8_t> blob_buf(max_blob_bytes, 0);
|
||||
bs_t blob;
|
||||
bs_init(&blob, blob_buf.data(), blob_buf.size());
|
||||
|
||||
bool first_coded = true;
|
||||
int skip_count = 0;
|
||||
|
||||
for (int i = first_nonskip; i <= last_nonskip; i++) {
|
||||
if (mbs[i].is_skip) {
|
||||
skip_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first_coded) {
|
||||
/* Write ue(skip_count) before this non-skip MB */
|
||||
bs_write_ue(&blob, (uint32_t)skip_count);
|
||||
skip_count = 0;
|
||||
} else {
|
||||
first_coded = false;
|
||||
skip_count = 0;
|
||||
}
|
||||
|
||||
/* Copy MB bitstream into blob */
|
||||
subcodec::mux::bs_copy_bits(&blob, mbs[i].buf, 0, mbs[i].bit_count);
|
||||
}
|
||||
|
||||
int blob_bits = bs_bit_count(&blob);
|
||||
auto [max_run, leading_zb, trailing_zb] = subcodec::mux::scan_zero_runs(blob_buf.data(), blob_bits);
|
||||
|
||||
/* Byte-align the blob */
|
||||
int blob_bytes = bs_byte_align(&blob);
|
||||
|
||||
/* Write 6-byte row header + blob to output */
|
||||
out.push_back(leading);
|
||||
out.push_back(trailing);
|
||||
uint16_t bbc = static_cast<uint16_t>(blob_bits);
|
||||
if (max_run >= 16) bbc |= 0x8000;
|
||||
out.push_back(static_cast<uint8_t>(bbc & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((bbc >> 8) & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>(std::min(leading_zb, 255)));
|
||||
out.push_back(static_cast<uint8_t>(std::min(trailing_zb, 255)));
|
||||
|
||||
/* Append blob bytes */
|
||||
out.insert(out.end(), blob_buf.data(), blob_buf.data() + blob_bytes);
|
||||
}
|
||||
|
||||
/* ---- Public API ---- */
|
||||
|
||||
MbsEncodedFrame encode_frame(
|
||||
const FrameParams& params,
|
||||
const MacroblockData* mbs) {
|
||||
|
||||
int width = params.width_mbs;
|
||||
int height = params.height_mbs;
|
||||
int num_mbs = width * height;
|
||||
if (num_mbs <= 0 || !mbs) return {};
|
||||
|
||||
MbsEncodedFrame result;
|
||||
result.data.reserve(4096);
|
||||
result.rows.resize(height);
|
||||
|
||||
/* Allocate context row for nC / MV prediction */
|
||||
std::vector<MbContext> ctx_row(width);
|
||||
MbContext ctx_left{};
|
||||
|
||||
/* Temporary per-MB encoded data for one row */
|
||||
std::vector<MbEncoded> row_mbs(width);
|
||||
|
||||
int err = 0;
|
||||
for (int mb_y = 0; mb_y < height && !err; mb_y++) {
|
||||
ctx_left = MbContext{};
|
||||
|
||||
for (int mb_x = 0; mb_x < width && !err; mb_x++) {
|
||||
int mb_idx = mb_y * width + mb_x;
|
||||
const MacroblockData* mb = &mbs[mb_idx];
|
||||
|
||||
MbContext* above = (mb_y > 0) ? &ctx_row[mb_x] : nullptr;
|
||||
MbContext* left_ptr = (mb_x > 0) ? &ctx_left : nullptr;
|
||||
MbContext* above_right = (mb_y > 0 && mb_x < width - 1)
|
||||
? &ctx_row[mb_x + 1] : nullptr;
|
||||
|
||||
MbContext out_ctx{};
|
||||
MbEncoded& enc = row_mbs[mb_x];
|
||||
|
||||
switch (mb->mb_type) {
|
||||
case MbType::SKIP:
|
||||
enc.is_skip = true;
|
||||
enc.bit_count = 0;
|
||||
break;
|
||||
case MbType::P_16x16: {
|
||||
enc.is_skip = false;
|
||||
memset(enc.buf, 0, sizeof(enc.buf));
|
||||
bs_t b;
|
||||
bs_init(&b, enc.buf, sizeof(enc.buf));
|
||||
err = encode_mb_p16x16_bs(&b, mb, left_ptr, above,
|
||||
above_right, &out_ctx);
|
||||
enc.bit_count = bs_bit_count(&b);
|
||||
break;
|
||||
}
|
||||
case MbType::I_16x16: {
|
||||
enc.is_skip = false;
|
||||
memset(enc.buf, 0, sizeof(enc.buf));
|
||||
bs_t b;
|
||||
bs_init(&b, enc.buf, sizeof(enc.buf));
|
||||
err = encode_mb_i16x16_bs(&b, mb, left_ptr, above, &out_ctx);
|
||||
enc.bit_count = bs_bit_count(&b);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
ctx_left = out_ctx;
|
||||
ctx_row[mb_x] = out_ctx;
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
/* Record where this row's data starts in result.data */
|
||||
size_t row_data_start = result.data.size();
|
||||
|
||||
/* Assemble row blob and append to result.data */
|
||||
assemble_row_blob(row_mbs.data(), width, result.data);
|
||||
|
||||
/* Parse the row descriptor from what we just wrote */
|
||||
MbsRow& row = result.rows[mb_y];
|
||||
const uint8_t* rp = result.data.data() + row_data_start;
|
||||
row.leading_skips = rp[0];
|
||||
row.trailing_skips = rp[1];
|
||||
row.blob_bit_count = static_cast<uint16_t>(rp[2]) |
|
||||
(static_cast<uint16_t>(rp[3]) << 8);
|
||||
row.leading_zero_bits = rp[4];
|
||||
row.trailing_zero_bits = rp[5];
|
||||
if (row.bit_count() > 0) {
|
||||
row.blob_data = rp + 6;
|
||||
} else {
|
||||
row.blob_data = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (err) return {};
|
||||
|
||||
/* Fix up blob_data pointers (they may have been invalidated by vector growth).
|
||||
* Re-parse from the final data buffer. */
|
||||
{
|
||||
const uint8_t* dp = result.data.data();
|
||||
for (int mb_y = 0; mb_y < height; mb_y++) {
|
||||
MbsRow& row = result.rows[mb_y];
|
||||
row.leading_skips = dp[0];
|
||||
row.trailing_skips = dp[1];
|
||||
row.blob_bit_count = static_cast<uint16_t>(dp[2]) |
|
||||
(static_cast<uint16_t>(dp[3]) << 8);
|
||||
row.leading_zero_bits = dp[4];
|
||||
row.trailing_zero_bits = dp[5];
|
||||
int blob_bytes = (row.bit_count() + 7) / 8;
|
||||
if (row.bit_count() > 0) {
|
||||
row.blob_data = dp + 6;
|
||||
} else {
|
||||
row.blob_data = nullptr;
|
||||
}
|
||||
dp += 6 + blob_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ---- Merged row blob (local copy for encode_frame_merged) ---- */
|
||||
|
||||
static MbsRow merge_color_alpha_row_local(
|
||||
const MbsRow& color, const MbsRow& alpha,
|
||||
int sprite_w, int padding,
|
||||
std::vector<uint8_t>& out_data) {
|
||||
|
||||
int slot_w = sprite_w * 2 - padding;
|
||||
bool has_color = color.bit_count() > 0;
|
||||
bool has_alpha = alpha.bit_count() > 0;
|
||||
|
||||
MbsRow merged;
|
||||
merged.blob_data = nullptr;
|
||||
|
||||
if (!has_color && !has_alpha) {
|
||||
merged.leading_skips = static_cast<uint8_t>(std::min(slot_w, 255));
|
||||
merged.trailing_skips = 0;
|
||||
merged.blob_bit_count = 0;
|
||||
merged.leading_zero_bits = 0;
|
||||
merged.trailing_zero_bits = 0;
|
||||
return merged;
|
||||
}
|
||||
|
||||
/* Compute merged leading/trailing skips */
|
||||
int merged_leading, merged_trailing;
|
||||
if (has_color) {
|
||||
merged_leading = color.leading_skips;
|
||||
} else {
|
||||
merged_leading = (sprite_w - padding) + alpha.leading_skips;
|
||||
}
|
||||
|
||||
if (has_alpha) {
|
||||
merged_trailing = alpha.trailing_skips;
|
||||
} else {
|
||||
merged_trailing = color.trailing_skips + (sprite_w - padding);
|
||||
}
|
||||
|
||||
/* Build merged blob bitstream */
|
||||
int max_bits = (has_color ? color.bit_count() : 0) +
|
||||
25 /* max ue bits */ +
|
||||
(has_alpha ? alpha.bit_count() : 0);
|
||||
int max_bytes = (max_bits + 7) / 8 + 4;
|
||||
size_t blob_start = out_data.size();
|
||||
out_data.resize(blob_start + max_bytes, 0);
|
||||
|
||||
bs_t bs;
|
||||
bs_init(&bs, out_data.data() + blob_start, max_bytes);
|
||||
|
||||
if (has_color) {
|
||||
subcodec::mux::bs_copy_bits(&bs, color.blob_data, 0, color.bit_count());
|
||||
}
|
||||
|
||||
if (has_color && has_alpha) {
|
||||
int inter_skip;
|
||||
if (padding >= alpha.leading_skips) {
|
||||
inter_skip = color.trailing_skips;
|
||||
} else {
|
||||
inter_skip = color.trailing_skips + alpha.leading_skips - padding;
|
||||
}
|
||||
bs_write_ue(&bs, static_cast<uint32_t>(inter_skip));
|
||||
}
|
||||
|
||||
if (has_alpha) {
|
||||
subcodec::mux::bs_copy_bits(&bs, alpha.blob_data, 0, alpha.bit_count());
|
||||
}
|
||||
|
||||
int merged_bits = static_cast<int>(bs.p - bs.start) * 8 + (8 - bs.bits_left);
|
||||
if (bs.bits_left < 8) { bs.p++; bs.bits_left = 8; }
|
||||
int merged_bytes = (merged_bits + 7) / 8;
|
||||
out_data.resize(blob_start + merged_bytes);
|
||||
|
||||
auto [max_run, leading_zb, trailing_zb] = subcodec::mux::scan_zero_runs(
|
||||
out_data.data() + blob_start, merged_bits);
|
||||
|
||||
merged.leading_skips = static_cast<uint8_t>(std::min(merged_leading, 255));
|
||||
merged.trailing_skips = static_cast<uint8_t>(std::min(merged_trailing, 255));
|
||||
uint16_t bbc = static_cast<uint16_t>(merged_bits & 0x7FFF);
|
||||
if (max_run >= 16) bbc |= 0x8000;
|
||||
merged.blob_bit_count = bbc;
|
||||
merged.leading_zero_bits = static_cast<uint8_t>(std::min(leading_zb, 255));
|
||||
merged.trailing_zero_bits = static_cast<uint8_t>(std::min(trailing_zb, 255));
|
||||
merged.blob_data = out_data.data() + blob_start;
|
||||
return merged;
|
||||
}
|
||||
|
||||
/* ---- encode_frame_merged ---- */
|
||||
|
||||
MbsEncodedFrame encode_frame_merged(
|
||||
const FrameParams& color_params, const MacroblockData* color_mbs,
|
||||
const FrameParams& alpha_params, const MacroblockData* alpha_mbs,
|
||||
int sprite_w, int padding) {
|
||||
|
||||
// Encode color and alpha halves separately
|
||||
auto color_ef = encode_frame(color_params, color_mbs);
|
||||
if (color_ef.data.empty()) return {};
|
||||
|
||||
auto alpha_ef = encode_frame(alpha_params, alpha_mbs);
|
||||
if (alpha_ef.data.empty()) return {};
|
||||
|
||||
int height = color_params.height_mbs;
|
||||
|
||||
// Merge each row pair
|
||||
MbsEncodedFrame result;
|
||||
result.rows.resize(height);
|
||||
|
||||
std::vector<uint8_t> merged_data;
|
||||
merged_data.reserve(color_ef.data.size() + alpha_ef.data.size());
|
||||
std::vector<size_t> blob_offsets(height, SIZE_MAX);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
size_t offset_before = merged_data.size();
|
||||
MbsRow merged = merge_color_alpha_row_local(
|
||||
color_ef.rows[y], alpha_ef.rows[y],
|
||||
sprite_w, padding, merged_data);
|
||||
|
||||
result.rows[y] = merged;
|
||||
if (merged.blob_data) {
|
||||
blob_offsets[y] = offset_before;
|
||||
}
|
||||
}
|
||||
|
||||
// Consolidate blob data and fix up pointers
|
||||
result.data = std::move(merged_data);
|
||||
for (int y = 0; y < height; y++) {
|
||||
if (blob_offsets[y] != SIZE_MAX) {
|
||||
result.rows[y].blob_data = result.data.data() + blob_offsets[y];
|
||||
}
|
||||
}
|
||||
|
||||
// Write serialized form: 6-byte header + blob bytes per row
|
||||
std::vector<uint8_t> serialized;
|
||||
serialized.reserve(result.data.size() + height * 6);
|
||||
for (int y = 0; y < height; y++) {
|
||||
const MbsRow& row = result.rows[y];
|
||||
serialized.push_back(row.leading_skips);
|
||||
serialized.push_back(row.trailing_skips);
|
||||
serialized.push_back(static_cast<uint8_t>(row.blob_bit_count & 0xFF));
|
||||
serialized.push_back(static_cast<uint8_t>((row.blob_bit_count >> 8) & 0xFF));
|
||||
serialized.push_back(row.leading_zero_bits);
|
||||
serialized.push_back(row.trailing_zero_bits);
|
||||
int blob_bytes = (row.bit_count() + 7) / 8;
|
||||
if (blob_bytes > 0 && row.blob_data) {
|
||||
serialized.insert(serialized.end(), row.blob_data, row.blob_data + blob_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace data with serialized form and re-parse row pointers
|
||||
result.data = std::move(serialized);
|
||||
const uint8_t* dp = result.data.data();
|
||||
for (int y = 0; y < height; y++) {
|
||||
MbsRow& row = result.rows[y];
|
||||
row.leading_skips = dp[0];
|
||||
row.trailing_skips = dp[1];
|
||||
row.blob_bit_count = static_cast<uint16_t>(dp[2]) | (static_cast<uint16_t>(dp[3]) << 8);
|
||||
row.leading_zero_bits = dp[4];
|
||||
row.trailing_zero_bits = dp[5];
|
||||
int blob_bytes = (row.bit_count() + 7) / 8;
|
||||
row.blob_data = (row.bit_count() > 0) ? dp + 6 : nullptr;
|
||||
dp += 6 + blob_bytes;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace subcodec::mbs
|
||||
25
third-party/subcodec/src/mbs_encode.h
vendored
Normal file
25
third-party/subcodec/src/mbs_encode.h
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "types.h"
|
||||
#include "mbs_format.h"
|
||||
|
||||
namespace subcodec::mbs {
|
||||
|
||||
MbsEncodedFrame encode_frame(
|
||||
const FrameParams& params,
|
||||
const MacroblockData* mbs);
|
||||
|
||||
// Encode color and alpha halves into a single frame with pre-merged row blobs.
|
||||
// color_params/color_mbs: color half MacroblockData (padded sprite dimensions).
|
||||
// alpha_params/alpha_mbs: alpha half MacroblockData (same dimensions).
|
||||
// sprite_w: padded sprite width in MBs. padding: always 1.
|
||||
// Returns MbsEncodedFrame with merged rows (no separate alpha_rows).
|
||||
MbsEncodedFrame encode_frame_merged(
|
||||
const FrameParams& color_params, const MacroblockData* color_mbs,
|
||||
const FrameParams& alpha_params, const MacroblockData* alpha_mbs,
|
||||
int sprite_w, int padding);
|
||||
|
||||
} // namespace subcodec::mbs
|
||||
9
third-party/subcodec/src/mbs_format.h
vendored
Normal file
9
third-party/subcodec/src/mbs_format.h
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
#include <cstdint>
|
||||
|
||||
// .mbs binary format constants (MB type codes in serialized frame data)
|
||||
inline constexpr uint32_t MBS_MAGIC_V6 = 0x3653424D; // 'MBS6' — pre-merged blobs
|
||||
inline constexpr int MBS_MB_SKIP = 0;
|
||||
inline constexpr int MBS_MB_P16x16 = 1;
|
||||
inline constexpr int MBS_MB_I16x16 = 2;
|
||||
1004
third-party/subcodec/src/mbs_mux_common.cpp
vendored
Normal file
1004
third-party/subcodec/src/mbs_mux_common.cpp
vendored
Normal file
File diff suppressed because it is too large
Load diff
398
third-party/subcodec/src/mbs_mux_common.h
vendored
Normal file
398
third-party/subcodec/src/mbs_mux_common.h
vendored
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
#include "types.h"
|
||||
#include "error.h"
|
||||
#include "bs.h"
|
||||
#include "mbs_format.h"
|
||||
#include "cavlc.h"
|
||||
|
||||
namespace subcodec::mux {
|
||||
|
||||
/* ---- Exp-golomb LUT ---- */
|
||||
|
||||
struct UeEntry {
|
||||
uint32_t pattern; /* bit pattern, MSB-first */
|
||||
uint8_t len; /* number of bits (max 25 for values up to 4095) */
|
||||
};
|
||||
|
||||
static constexpr int UE_LUT_SIZE = 4096;
|
||||
extern UeEntry ue_lut[UE_LUT_SIZE];
|
||||
|
||||
void build_ue_lut();
|
||||
|
||||
/* ---- EbspWriter: single-pass direct EBSP output ---- */
|
||||
|
||||
/* Writes bits directly to an output buffer with inline EBSP escape byte
|
||||
* insertion (0x00 0x00 [0x00-0x03] → 0x00 0x00 0x03 [0x00-0x03]).
|
||||
* Replaces the two-stage bs_t→RBSP + rbsp_to_ebsp pipeline. */
|
||||
struct EbspWriter {
|
||||
uint8_t* out; /* next complete-byte write position */
|
||||
uint32_t partial; /* accumulated bits not yet flushed (MSB-first) */
|
||||
int bits; /* number of valid bits in partial (0-7) */
|
||||
int zero_count; /* consecutive 0x00 bytes written (for EBSP escaping) */
|
||||
|
||||
/* Write one complete byte with EBSP escape check. */
|
||||
inline void flush_byte(uint8_t byte) {
|
||||
if (zero_count >= 2 && byte <= 3) {
|
||||
*out++ = 0x03;
|
||||
zero_count = 0;
|
||||
}
|
||||
*out++ = byte;
|
||||
zero_count = (byte == 0) ? zero_count + 1 : 0;
|
||||
}
|
||||
|
||||
/* Accumulate n bits (max 25) and flush complete bytes.
|
||||
* val must have its bits in the low n positions. */
|
||||
inline void write_bits(uint32_t val, int n) {
|
||||
partial = (partial << n) | val;
|
||||
bits += n;
|
||||
while (bits >= 8) {
|
||||
bits -= 8;
|
||||
flush_byte(static_cast<uint8_t>((partial >> bits) & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
/* Write unsigned exp-golomb code. Uses LUT for values < 4096,
|
||||
* falls back to computed encoding for larger values. */
|
||||
inline void write_ue(uint32_t val) {
|
||||
if (val < UE_LUT_SIZE) {
|
||||
const auto& e = ue_lut[val];
|
||||
write_bits(e.pattern, e.len);
|
||||
} else {
|
||||
/* Fallback: compute exp-golomb encoding */
|
||||
uint32_t v = val + 1;
|
||||
int len = 0;
|
||||
uint32_t tmp = v;
|
||||
while (tmp > 0) { tmp >>= 1; len++; }
|
||||
write_bits(v, 2 * len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Write signed exp-golomb code. */
|
||||
inline void write_se(int32_t val) {
|
||||
if (val <= 0)
|
||||
write_ue(static_cast<uint32_t>(-val * 2));
|
||||
else
|
||||
write_ue(static_cast<uint32_t>(val * 2 - 1));
|
||||
}
|
||||
|
||||
/* Bulk write complete bytes with EBSP escaping.
|
||||
* Requires bits == 0 (byte-aligned). Uses NEON to detect zero bytes
|
||||
* and bulk-copy safe regions. For I_PCM pixel data. */
|
||||
void flush_bytes(const uint8_t* src, int nbytes);
|
||||
|
||||
/* Bulk copy blob bits into output with inline EBSP escaping.
|
||||
* src is a byte array; nbits bits starting from bit 0 are copied.
|
||||
* Handles both aligned (bits == 0) and non-aligned cases. */
|
||||
void copy_blob(const uint8_t* src, int nbits);
|
||||
|
||||
/* Bulk copy blob bits with fast path when blob has no 16+ consecutive zero bits.
|
||||
* has_long_zero_run: if false, interior bytes are guaranteed EBSP-safe.
|
||||
* leading_zero_bits/trailing_zero_bits: for boundary handling. */
|
||||
void copy_blob(const uint8_t* src, int nbits,
|
||||
bool has_long_zero_run, uint8_t leading_zero_bits,
|
||||
uint8_t trailing_zero_bits);
|
||||
};
|
||||
|
||||
/* ---- RbspWriter: branchless bitstream writer for RBSP staging ---- */
|
||||
|
||||
/* No EBSP escape checking — caller must run rbsp_to_ebsp after.
|
||||
* Used in the two-pass P-frame mux path. */
|
||||
struct RbspWriter {
|
||||
uint8_t* out; /* next complete-byte write position */
|
||||
uint32_t partial; /* accumulated bits not yet flushed (MSB-first) */
|
||||
int bits; /* number of valid bits in partial (0-7) */
|
||||
|
||||
inline void write_bits(uint32_t val, int n) {
|
||||
partial = (partial << n) | val;
|
||||
bits += n;
|
||||
while (bits >= 8) {
|
||||
bits -= 8;
|
||||
*out++ = static_cast<uint8_t>((partial >> bits) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
inline void write_ue(uint32_t val) {
|
||||
if (val < UE_LUT_SIZE) {
|
||||
const auto& e = ue_lut[val];
|
||||
write_bits(e.pattern, e.len);
|
||||
} else {
|
||||
uint32_t v = val + 1;
|
||||
int len = 0;
|
||||
uint32_t tmp = v;
|
||||
while (tmp > 0) { tmp >>= 1; len++; }
|
||||
write_bits(v, 2 * len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
inline void write_se(int32_t val) {
|
||||
if (val <= 0) write_ue(static_cast<uint32_t>(-val * 2));
|
||||
else write_ue(static_cast<uint32_t>(val * 2 - 1));
|
||||
}
|
||||
|
||||
/* Bulk copy blob bits — no EBSP checking.
|
||||
* Aligned: memcpy. Non-aligned: NEON shift+write. */
|
||||
void copy_blob(const uint8_t* src, int nbits);
|
||||
};
|
||||
|
||||
/* ---- Coeff_token LUT ---- */
|
||||
|
||||
void build_ct_lut();
|
||||
|
||||
/* ---- Grid layout helpers ---- */
|
||||
|
||||
int ceil_div(int a, int b);
|
||||
int ceil_sqrt(int n);
|
||||
|
||||
/* ---- Row plan types (precomputed composite layout) ---- */
|
||||
|
||||
struct RowOp {
|
||||
uint16_t slot_idx; /* which slot this sprite is in */
|
||||
uint16_t sprite_row; /* which row within the sprite */
|
||||
uint16_t pre_skip; /* composite skip MBs from previous sprite-region end
|
||||
(or row start) to this sprite-region start.
|
||||
Clamped to >= 0. Fixed for a given layout. */
|
||||
uint16_t overlap; /* MBs at start of this sprite's region already covered
|
||||
by the previous sprite (shared padding). The mux loop
|
||||
uses this as `already_inside` — same role as in the
|
||||
current grid walk code. 0 for the first sprite in a row
|
||||
or when there's a gap between sprites. */
|
||||
};
|
||||
|
||||
struct CompositeRowPlan {
|
||||
uint16_t trailing_skips; /* composite skip MBs after last sprite-region end */
|
||||
uint16_t ops_offset; /* index into flat ops array */
|
||||
uint16_t ops_count; /* number of RowOps in this row */
|
||||
};
|
||||
|
||||
/* Build precomputed row plans from slot active state.
|
||||
* Called from MuxSurface::add_sprite / remove_sprite. */
|
||||
void build_row_plans(
|
||||
const bool* slot_active, int max_slots,
|
||||
int sprite_w, int sprite_h, int padding,
|
||||
int total_w, int total_h,
|
||||
std::vector<CompositeRowPlan>& row_plans,
|
||||
std::vector<RowOp>& row_ops);
|
||||
|
||||
/* Pre-resolved blob operation for the tight mux loop.
|
||||
* Built once per frame from row_ops + slot state.
|
||||
* Only active blobs with data appear — inactive slots and
|
||||
* all-skip rows are folded into skip counts. */
|
||||
struct MicroOp {
|
||||
const uint8_t* blob_data;
|
||||
uint16_t blob_bits; /* bit count (lower 15 bits of blob_bit_count) */
|
||||
uint16_t skip; /* composite skip MBs to write before this blob */
|
||||
uint8_t flags; /* [0] = has_long_zero_run */
|
||||
uint8_t leading_zb;
|
||||
uint8_t trailing_zb;
|
||||
uint8_t _pad;
|
||||
};
|
||||
|
||||
/* ---- Zero-run scanning ---- */
|
||||
|
||||
/* Scan blob bits for zero-run metadata.
|
||||
* Returns: {max_consecutive_zero_bits, leading_zero_bits, trailing_zero_bits} */
|
||||
std::tuple<int, int, int> scan_zero_runs(const uint8_t* blob, int blob_bits);
|
||||
|
||||
/* ---- Bit writing helpers ---- */
|
||||
|
||||
/* Bulk copy nbits from src byte array at src_bit_offset into dst bs_t.
|
||||
* Uses byte-level operations where possible for speed. */
|
||||
void bs_copy_bits(bs_t* dst, const uint8_t* src, int src_bit_offset, int nbits);
|
||||
|
||||
/* ---- RBSP to EBSP ---- */
|
||||
|
||||
size_t rbsp_to_ebsp(const uint8_t* rbsp, size_t rbsp_size,
|
||||
uint8_t* ebsp, size_t ebsp_size);
|
||||
|
||||
/* NEON-accelerated EBSP escape insertion.
|
||||
* Scans 16 bytes at a time for zero bytes; bulk-copies safe regions.
|
||||
* Returns output byte count, 0 on error. */
|
||||
size_t rbsp_to_ebsp_neon(const uint8_t* rbsp, size_t rbsp_size,
|
||||
uint8_t* ebsp, size_t ebsp_size);
|
||||
|
||||
/* ---- Frame writers ---- */
|
||||
|
||||
/* Write all-black IDR frame (I_16x16 DC prediction for every MB). */
|
||||
std::expected<size_t, Error> write_idr_black(
|
||||
int total_w, int total_h,
|
||||
int8_t qp_delta_idr, int log2_max_frame_num,
|
||||
std::span<uint8_t> output);
|
||||
|
||||
/* Write all-I_PCM IDR frame from caller-provided decoded YUV planes.
|
||||
* Every MB is I_PCM (384 raw bytes: 256 luma + 64 Cb + 64 Cr).
|
||||
* Inline: enables cross-TU inlining into MuxSurface::resize at -O2. */
|
||||
inline std::expected<size_t, Error> write_idr_ipcm(
|
||||
int total_w, int total_h,
|
||||
int log2_max_frame_num,
|
||||
const uint8_t* y_plane, int stride_y,
|
||||
const uint8_t* cb_plane, int stride_cb,
|
||||
const uint8_t* cr_plane, int stride_cr,
|
||||
std::span<uint8_t> output) {
|
||||
|
||||
int num_mbs = total_w * total_h;
|
||||
size_t needed = static_cast<size_t>(num_mbs) * 580 + 4096;
|
||||
if (output.size() < needed)
|
||||
return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
uint8_t* buf = output.data();
|
||||
|
||||
/* NAL header: nal_ref_idc=3, nal_unit_type=5 (IDR) */
|
||||
buf[0] = 0x00; buf[1] = 0x00; buf[2] = 0x00; buf[3] = 0x01;
|
||||
buf[4] = (3 << 5) | 5;
|
||||
|
||||
/* Single-pass: write directly to EBSP output via EbspWriter. */
|
||||
EbspWriter w;
|
||||
w.out = buf + 5;
|
||||
w.partial = 0;
|
||||
w.bits = 0;
|
||||
w.zero_count = 0;
|
||||
|
||||
/* Slice header */
|
||||
w.write_ue(0); /* first_mb_in_slice */
|
||||
w.write_ue(7); /* slice_type = I */
|
||||
w.write_ue(0); /* pps_id */
|
||||
w.write_bits(0, log2_max_frame_num); /* frame_num = 0 */
|
||||
w.write_ue(0); /* idr_pic_id */
|
||||
w.write_bits(0, 1); /* no_output_of_prior_pics_flag */
|
||||
w.write_bits(0, 1); /* long_term_reference_flag */
|
||||
w.write_se(0); /* slice_qp_delta = 0 */
|
||||
w.write_ue(1); /* disable_deblocking_filter_idc */
|
||||
|
||||
/* Precompute EBSP-escaped all-zero luma pattern (256 input zeros → 384 output bytes).
|
||||
* After mb_type + alignment, zero_count == 1 (from alignment byte 0x00).
|
||||
* Each pair of input zeros produces [0x00, 0x03, 0x00] (3 bytes). */
|
||||
uint8_t black_luma_ebsp[384];
|
||||
for (int i = 0; i < 128; i++) {
|
||||
black_luma_ebsp[i * 3 + 0] = 0x00;
|
||||
black_luma_ebsp[i * 3 + 1] = 0x03;
|
||||
black_luma_ebsp[i * 3 + 2] = 0x00;
|
||||
}
|
||||
|
||||
uint8_t neutral_chroma[128];
|
||||
memset(neutral_chroma, 0x80, 128);
|
||||
|
||||
/* Write MBs — fast path for all-black MBs (Y=0, Cb=Cr=128). */
|
||||
for (int mb_idx = 0; mb_idx < num_mbs; mb_idx++) {
|
||||
int mb_x = mb_idx % total_w;
|
||||
int mb_y = mb_idx / total_w;
|
||||
|
||||
w.write_ue(25); /* mb_type = I_PCM */
|
||||
|
||||
if (w.bits > 0) {
|
||||
w.write_bits(0, 8 - w.bits); /* byte-align */
|
||||
}
|
||||
|
||||
int y_base = mb_y * 16;
|
||||
int x_base = mb_x * 16;
|
||||
int cb_y_base = mb_y * 8;
|
||||
int cb_x_base = mb_x * 8;
|
||||
|
||||
bool is_black = true;
|
||||
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
|
||||
{
|
||||
uint8x16_t vzero = vdupq_n_u8(0);
|
||||
uint8x16_t v128 = vdupq_n_u8(128);
|
||||
for (int row = 0; row < 16 && is_black; row++) {
|
||||
uint8x16_t v = vld1q_u8(y_plane + (y_base + row) * stride_y + x_base);
|
||||
if (vmaxvq_u8(v) != 0) is_black = false;
|
||||
}
|
||||
for (int row = 0; row < 8 && is_black; row++) {
|
||||
uint8x8_t v = vld1_u8(cb_plane + (cb_y_base + row) * stride_cb + cb_x_base);
|
||||
uint8x8_t cmp = vceq_u8(v, vget_low_u8(v128));
|
||||
if (vminv_u8(cmp) == 0) is_black = false;
|
||||
}
|
||||
for (int row = 0; row < 8 && is_black; row++) {
|
||||
uint8x8_t v = vld1_u8(cr_plane + (cb_y_base + row) * stride_cr + cb_x_base);
|
||||
uint8x8_t cmp = vceq_u8(v, vget_low_u8(v128));
|
||||
if (vminv_u8(cmp) == 0) is_black = false;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (y_plane[y_base * stride_y + x_base] != 0) is_black = false;
|
||||
if (is_black && cb_plane[cb_y_base * stride_cb + cb_x_base] != 128) is_black = false;
|
||||
#endif
|
||||
|
||||
if (is_black) {
|
||||
memcpy(w.out, black_luma_ebsp, 384);
|
||||
w.out += 384;
|
||||
memcpy(w.out, neutral_chroma, 128);
|
||||
w.out += 128;
|
||||
w.zero_count = 0;
|
||||
} else {
|
||||
/* Gather MB samples into contiguous buffer for bulk EBSP processing.
|
||||
* 256 luma + 64 Cb + 64 Cr = 384 bytes → flush_bytes processes
|
||||
* 24 NEON chunks (16 bytes each) instead of 384 scalar flush_byte calls. */
|
||||
uint8_t mb_buf[384];
|
||||
uint8_t* dst = mb_buf;
|
||||
for (int row = 0; row < 16; row++) {
|
||||
memcpy(dst, y_plane + (y_base + row) * stride_y + x_base, 16);
|
||||
dst += 16;
|
||||
}
|
||||
for (int row = 0; row < 8; row++) {
|
||||
memcpy(dst, cb_plane + (cb_y_base + row) * stride_cb + cb_x_base, 8);
|
||||
dst += 8;
|
||||
}
|
||||
for (int row = 0; row < 8; row++) {
|
||||
memcpy(dst, cr_plane + (cb_y_base + row) * stride_cr + cb_x_base, 8);
|
||||
dst += 8;
|
||||
}
|
||||
w.flush_bytes(mb_buf, 384);
|
||||
}
|
||||
}
|
||||
|
||||
w.write_bits(1, 1); /* RBSP trailing bits */
|
||||
if (w.bits > 0) {
|
||||
w.write_bits(0, 8 - w.bits);
|
||||
}
|
||||
|
||||
return static_cast<size_t>(w.out - buf);
|
||||
}
|
||||
|
||||
/* Per-slot info needed by the row-blob mux path */
|
||||
struct SlotInfo {
|
||||
const MbsSprite* sprite = nullptr;
|
||||
int frame_index = 0;
|
||||
};
|
||||
|
||||
/* Build flat micro-op array from row plans and current frame state.
|
||||
* Returns trailing skip count (MBs after the last blob). */
|
||||
int build_micro_ops(
|
||||
const SlotInfo* slots,
|
||||
const CompositeRowPlan* row_plans, int num_rows,
|
||||
const RowOp* row_ops,
|
||||
int sprite_w, int padding,
|
||||
std::vector<MicroOp>& ops);
|
||||
|
||||
/* Two-pass P-frame writer using pre-resolved micro-ops.
|
||||
* Pass 1: write to RBSP staging buffer via RbspWriter (no EBSP checking).
|
||||
* Pass 2: NEON-accelerated EBSP escape insertion.
|
||||
* micro_ops/trailing_skip: from build_micro_ops().
|
||||
* rbsp_buf: staging buffer (caller-owned, at least output.size() bytes). */
|
||||
std::expected<size_t, Error> write_p_frame_rbsp(
|
||||
const MicroOp* micro_ops, int num_ops, int trailing_skip,
|
||||
int frame_idx, int log2_max_frame_num,
|
||||
int8_t qp_delta_p,
|
||||
std::span<uint8_t> rbsp_buf,
|
||||
std::span<uint8_t> output);
|
||||
|
||||
/* Single-pass P-frame writer using pre-resolved micro-ops + EbspWriter.
|
||||
* Uses EbspWriter's inline EBSP escaping with fast-path for escape-free blobs.
|
||||
* micro_ops/trailing_skip: from build_micro_ops(). */
|
||||
std::expected<size_t, Error> write_p_frame_micro(
|
||||
const MicroOp* micro_ops, int num_ops, int trailing_skip,
|
||||
int frame_idx, int log2_max_frame_num,
|
||||
int8_t qp_delta_p,
|
||||
std::span<uint8_t> output);
|
||||
|
||||
} // namespace subcodec::mux
|
||||
486
third-party/subcodec/src/mux_surface.cpp
vendored
Normal file
486
third-party/subcodec/src/mux_surface.cpp
vendored
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
#include "mux_surface.h"
|
||||
#include "frame_writer.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
std::expected<MuxSurface, Error>
|
||||
MuxSurface::create(const Params& params, FrameSink sink) {
|
||||
if (params.max_slots <= 0) return std::unexpected(Error::INVALID_INPUT);
|
||||
if (params.sprite_width <= 0 || params.sprite_width % 16 != 0 ||
|
||||
params.sprite_height <= 0 || params.sprite_height % 16 != 0)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
constexpr int padding_mbs = 1;
|
||||
int content_w_mbs = params.sprite_width / 16;
|
||||
int content_h_mbs = params.sprite_height / 16;
|
||||
int sw = content_w_mbs + 2 * padding_mbs;
|
||||
int sh = content_h_mbs + 2 * padding_mbs;
|
||||
int slot_w = sw * 2 - padding_mbs;
|
||||
int stride_x = slot_w - padding_mbs;
|
||||
int stride_y = sh - padding_mbs;
|
||||
|
||||
mux::build_ct_lut();
|
||||
mux::build_ue_lut();
|
||||
|
||||
int cols = mux::ceil_sqrt(params.max_slots);
|
||||
int rows = mux::ceil_div(params.max_slots, cols);
|
||||
int total_w = stride_x * cols + padding_mbs;
|
||||
int total_h = stride_y * rows + padding_mbs;
|
||||
int num_mbs = total_w * total_h;
|
||||
|
||||
MuxSurface s;
|
||||
s.params_ = params;
|
||||
s.sprite_w_mbs_ = sw;
|
||||
s.sprite_h_mbs_ = sh;
|
||||
s.content_w_ = params.sprite_width;
|
||||
s.content_h_ = params.sprite_height;
|
||||
s.total_w_ = total_w;
|
||||
s.total_h_ = total_h;
|
||||
s.cols_ = cols;
|
||||
s.rows_ = rows;
|
||||
s.stride_x_ = stride_x;
|
||||
s.stride_y_ = stride_y;
|
||||
s.frame_num_ = 0;
|
||||
s.num_mbs_ = num_mbs;
|
||||
s.slots_.resize(static_cast<size_t>(params.max_slots));
|
||||
s.slot_infos_.resize(static_cast<size_t>(params.max_slots));
|
||||
|
||||
s.buf_size_ = static_cast<size_t>(num_mbs) * 600 + 4096;
|
||||
s.buf_ = std::make_unique_for_overwrite<uint8_t[]>(s.buf_size_);
|
||||
s.rbsp_buf_size_ = s.buf_size_;
|
||||
s.rbsp_buf_ = std::make_unique_for_overwrite<uint8_t[]>(s.rbsp_buf_size_);
|
||||
s.micro_ops_.reserve(static_cast<size_t>(params.max_slots) * static_cast<size_t>(sh) * 2);
|
||||
|
||||
FrameParams fp{};
|
||||
fp.width_mbs = static_cast<uint16_t>(total_w);
|
||||
fp.height_mbs = static_cast<uint16_t>(total_h);
|
||||
fp.qp = params.qp;
|
||||
fp.log2_max_frame_num = static_cast<uint8_t>(s.log2_max_frame_num_);
|
||||
|
||||
std::span<uint8_t> out{s.buf_.get(), s.buf_size_};
|
||||
size_t offset = frame_writer::write_headers(out, fp);
|
||||
if (offset == 0) return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
auto idr_result = mux::write_idr_black(total_w, total_h,
|
||||
params.qp_delta_idr,
|
||||
s.log2_max_frame_num_,
|
||||
out.subspan(offset));
|
||||
if (!idr_result) return std::unexpected(idr_result.error());
|
||||
offset += *idr_result;
|
||||
|
||||
sink(std::span<const uint8_t>{s.buf_.get(), offset});
|
||||
|
||||
return std::move(s);
|
||||
}
|
||||
|
||||
std::expected<MuxSurface::SpriteRegion, Error> MuxSurface::add_sprite(const std::filesystem::path& mbs_path) {
|
||||
int slot_idx = -1;
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
if (!slots_[i].sprite) {
|
||||
slot_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot_idx < 0) return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
auto result = MbsSprite::load(mbs_path);
|
||||
if (!result) return std::unexpected(result.error());
|
||||
|
||||
if (result->width_mbs != sprite_w_mbs_ ||
|
||||
result->height_mbs != sprite_h_mbs_) {
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
}
|
||||
|
||||
slots_[slot_idx].sprite = std::move(*result);
|
||||
slots_[slot_idx].current_frame = 0;
|
||||
slots_[slot_idx].active = true;
|
||||
slots_[slot_idx].needs_emit = true;
|
||||
plans_dirty_ = true;
|
||||
dirty_ = true;
|
||||
|
||||
constexpr int padding_px = 16;
|
||||
int col = slot_idx % cols_;
|
||||
int row = slot_idx / cols_;
|
||||
int color_x = col * stride_x_ * 16 + padding_px;
|
||||
int color_y = row * stride_y_ * 16 + padding_px;
|
||||
int content_w_mbs = sprite_w_mbs_ - 2;
|
||||
int alpha_x = color_x + (content_w_mbs + 1) * 16;
|
||||
|
||||
SpriteRegion region;
|
||||
region.slot = slot_idx;
|
||||
region.color = {color_x, color_y, content_w_, content_h_};
|
||||
region.alpha = {alpha_x, color_y, content_w_, content_h_};
|
||||
return region;
|
||||
}
|
||||
|
||||
std::expected<MuxSurface::SpriteRegion, Error> MuxSurface::add_sprite(MbsSprite sprite) {
|
||||
int slot_idx = -1;
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
if (!slots_[i].sprite) {
|
||||
slot_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot_idx < 0) return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
if (sprite.width_mbs != sprite_w_mbs_ ||
|
||||
sprite.height_mbs != sprite_h_mbs_) {
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
}
|
||||
|
||||
slots_[slot_idx].sprite = std::move(sprite);
|
||||
slots_[slot_idx].current_frame = 0;
|
||||
slots_[slot_idx].active = true;
|
||||
slots_[slot_idx].needs_emit = true;
|
||||
plans_dirty_ = true;
|
||||
dirty_ = true;
|
||||
|
||||
constexpr int padding_px = 16;
|
||||
int col = slot_idx % cols_;
|
||||
int row = slot_idx / cols_;
|
||||
int color_x = col * stride_x_ * 16 + padding_px;
|
||||
int color_y = row * stride_y_ * 16 + padding_px;
|
||||
int content_w_mbs = sprite_w_mbs_ - 2;
|
||||
int alpha_x = color_x + (content_w_mbs + 1) * 16;
|
||||
|
||||
SpriteRegion region;
|
||||
region.slot = slot_idx;
|
||||
region.color = {color_x, color_y, content_w_, content_h_};
|
||||
region.alpha = {alpha_x, color_y, content_w_, content_h_};
|
||||
return region;
|
||||
}
|
||||
|
||||
void MuxSurface::remove_sprite(int slot) {
|
||||
if (slot < 0 || slot >= params_.max_slots) return;
|
||||
slots_[slot].sprite.reset();
|
||||
slots_[slot].active = false;
|
||||
slots_[slot].current_frame = 0;
|
||||
plans_dirty_ = true;
|
||||
}
|
||||
|
||||
CompactionInfo MuxSurface::check_compaction_opportunity() const {
|
||||
int active = 0;
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
if (slots_[i].active) active++;
|
||||
}
|
||||
|
||||
int min_grid_mbs = 0;
|
||||
if (active > 0) {
|
||||
int cols = mux::ceil_sqrt(active);
|
||||
int rows = mux::ceil_div(active, cols);
|
||||
int slot_w = sprite_w_mbs_ * 2 - 1; /* padding_mbs = 1 */
|
||||
int sx = slot_w - 1;
|
||||
int sy = sprite_h_mbs_ - 1;
|
||||
int tw = sx * cols + 1;
|
||||
int th = sy * rows + 1;
|
||||
min_grid_mbs = tw * th;
|
||||
}
|
||||
|
||||
return CompactionInfo{
|
||||
active,
|
||||
params_.max_slots,
|
||||
total_w_ * total_h_,
|
||||
min_grid_mbs
|
||||
};
|
||||
}
|
||||
|
||||
std::expected<MuxSurface::ResizeResult, Error> MuxSurface::resize(
|
||||
int new_max_slots,
|
||||
std::span<const uint8_t> decoded_y,
|
||||
std::span<const uint8_t> decoded_cb,
|
||||
std::span<const uint8_t> decoded_cr,
|
||||
int decoded_width,
|
||||
int decoded_height,
|
||||
int stride_y,
|
||||
int stride_cb,
|
||||
int stride_cr,
|
||||
FrameSink sink) {
|
||||
|
||||
/* Count active sprites */
|
||||
int active_count = 0;
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
if (slots_[i].active) active_count++;
|
||||
}
|
||||
|
||||
/* Validate */
|
||||
if (new_max_slots < active_count)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
if (decoded_width != total_w_ * 16 || decoded_height != total_h_ * 16)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
/* Collect active sprites with their old slot positions */
|
||||
struct SpriteInfo {
|
||||
MbsSprite sprite;
|
||||
int current_frame;
|
||||
int old_col, old_row;
|
||||
};
|
||||
std::vector<SpriteInfo> active_sprites;
|
||||
active_sprites.reserve(active_count);
|
||||
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
if (slots_[i].active && slots_[i].sprite) {
|
||||
int col = i % cols_;
|
||||
int row = i / cols_;
|
||||
active_sprites.push_back({
|
||||
std::move(*slots_[i].sprite),
|
||||
slots_[i].current_frame,
|
||||
col, row
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* Save old grid layout for pixel remapping */
|
||||
int old_stride_x = stride_x_;
|
||||
int old_stride_y_val = stride_y_;
|
||||
|
||||
/* Compute new grid layout (same algorithm as create()) */
|
||||
constexpr int padding_mbs = 1;
|
||||
int slot_w = sprite_w_mbs_ * 2 - padding_mbs;
|
||||
int new_stride_x = slot_w - padding_mbs;
|
||||
int new_stride_y_val = sprite_h_mbs_ - padding_mbs;
|
||||
|
||||
int new_cols = mux::ceil_sqrt(new_max_slots);
|
||||
int new_rows = mux::ceil_div(new_max_slots, new_cols);
|
||||
int new_total_w = new_stride_x * new_cols + padding_mbs;
|
||||
int new_total_h = new_stride_y_val * new_rows + padding_mbs;
|
||||
int new_num_mbs = new_total_w * new_total_h;
|
||||
|
||||
/* Build the remapped YUV planes for the new grid */
|
||||
int new_w_px = new_total_w * 16;
|
||||
int new_h_px = new_total_h * 16;
|
||||
int new_cw = new_w_px / 2;
|
||||
int new_ch = new_h_px / 2;
|
||||
|
||||
size_t y_size = static_cast<size_t>(new_w_px) * new_h_px;
|
||||
size_t c_size = static_cast<size_t>(new_cw) * new_ch;
|
||||
auto new_y_buf = std::make_unique_for_overwrite<uint8_t[]>(y_size);
|
||||
auto new_cb_buf = std::make_unique_for_overwrite<uint8_t[]>(c_size);
|
||||
auto new_cr_buf = std::make_unique_for_overwrite<uint8_t[]>(c_size);
|
||||
memset(new_y_buf.get(), 0, y_size);
|
||||
memset(new_cb_buf.get(), 128, c_size);
|
||||
memset(new_cr_buf.get(), 128, c_size);
|
||||
uint8_t* new_y = new_y_buf.get();
|
||||
uint8_t* new_cb = new_cb_buf.get();
|
||||
uint8_t* new_cr = new_cr_buf.get();
|
||||
|
||||
/* Copy sprite pixel regions from old to new positions */
|
||||
int sprite_h_px = sprite_h_mbs_ * 16;
|
||||
int slot_w_px = slot_w * 16;
|
||||
|
||||
for (int si = 0; si < (int)active_sprites.size(); si++) {
|
||||
auto& sp = active_sprites[si];
|
||||
|
||||
int old_x_px = sp.old_col * old_stride_x * 16;
|
||||
int old_y_px = sp.old_row * old_stride_y_val * 16;
|
||||
|
||||
int new_col = si % new_cols;
|
||||
int new_row = si / new_cols;
|
||||
int new_x_px = new_col * new_stride_x * 16;
|
||||
int new_y_px = new_row * new_stride_y_val * 16;
|
||||
|
||||
/* Copy luma */
|
||||
for (int r = 0; r < sprite_h_px; r++) {
|
||||
const uint8_t* src = decoded_y.data() + (old_y_px + r) * stride_y + old_x_px;
|
||||
uint8_t* dst = new_y + (new_y_px + r) * new_w_px + new_x_px;
|
||||
memcpy(dst, src, slot_w_px);
|
||||
}
|
||||
|
||||
/* Copy chroma */
|
||||
int old_cx = old_x_px / 2;
|
||||
int old_cy = old_y_px / 2;
|
||||
int new_cx = new_x_px / 2;
|
||||
int new_cy = new_y_px / 2;
|
||||
int slot_cw = slot_w_px / 2;
|
||||
int sprite_ch = sprite_h_px / 2;
|
||||
|
||||
for (int r = 0; r < sprite_ch; r++) {
|
||||
memcpy(new_cb + (new_cy + r) * new_cw + new_cx,
|
||||
decoded_cb.data() + (old_cy + r) * stride_cb + old_cx, slot_cw);
|
||||
memcpy(new_cr + (new_cy + r) * new_cw + new_cx,
|
||||
decoded_cr.data() + (old_cy + r) * stride_cr + old_cx, slot_cw);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reallocate frame buffers if new grid is larger.
|
||||
* buf_ at 600 bytes/MB is sufficient for both I_PCM (580 bytes/MB)
|
||||
* and subsequent P-frames. rbsp_buf_ is used for I_PCM output. */
|
||||
size_t new_buf_size = static_cast<size_t>(new_num_mbs) * 600 + 4096;
|
||||
if (new_buf_size > buf_size_) {
|
||||
buf_size_ = new_buf_size;
|
||||
buf_ = std::make_unique_for_overwrite<uint8_t[]>(buf_size_);
|
||||
rbsp_buf_size_ = buf_size_;
|
||||
rbsp_buf_ = std::make_unique_for_overwrite<uint8_t[]>(rbsp_buf_size_);
|
||||
}
|
||||
|
||||
/* Write new SPS/PPS into buf_ */
|
||||
FrameParams fp{};
|
||||
fp.width_mbs = static_cast<uint16_t>(new_total_w);
|
||||
fp.height_mbs = static_cast<uint16_t>(new_total_h);
|
||||
fp.qp = params_.qp;
|
||||
fp.log2_max_frame_num = static_cast<uint8_t>(log2_max_frame_num_);
|
||||
|
||||
std::span<uint8_t> hdr_out{buf_.get(), buf_size_};
|
||||
size_t hdr_offset = frame_writer::write_headers(hdr_out, fp);
|
||||
if (hdr_offset == 0) return std::unexpected(Error::OUT_OF_SPACE);
|
||||
|
||||
/* Write I_PCM IDR into rbsp_buf_ (reused, avoids separate allocation) */
|
||||
auto idr_result = mux::write_idr_ipcm(
|
||||
new_total_w, new_total_h,
|
||||
log2_max_frame_num_,
|
||||
new_y, new_w_px,
|
||||
new_cb, new_cw,
|
||||
new_cr, new_cw,
|
||||
{rbsp_buf_.get(), rbsp_buf_size_});
|
||||
|
||||
if (!idr_result) return std::unexpected(idr_result.error());
|
||||
|
||||
/* Emit SPS+PPS then I_PCM IDR */
|
||||
sink(std::span<const uint8_t>{buf_.get(), hdr_offset});
|
||||
sink(std::span<const uint8_t>{rbsp_buf_.get(), *idr_result});
|
||||
|
||||
/* Update internal state */
|
||||
params_.max_slots = new_max_slots;
|
||||
total_w_ = new_total_w;
|
||||
total_h_ = new_total_h;
|
||||
cols_ = new_cols;
|
||||
rows_ = new_rows;
|
||||
stride_x_ = new_stride_x;
|
||||
stride_y_ = new_stride_y_val;
|
||||
num_mbs_ = new_num_mbs;
|
||||
frame_num_ = 0;
|
||||
|
||||
/* Resize slot arrays */
|
||||
slots_.clear();
|
||||
slots_.resize(static_cast<size_t>(new_max_slots));
|
||||
slot_infos_.resize(static_cast<size_t>(new_max_slots));
|
||||
micro_ops_.reserve(static_cast<size_t>(new_max_slots) *
|
||||
static_cast<size_t>(sprite_h_mbs_) * 2);
|
||||
|
||||
/* Assign sprites to compacted slots and build result */
|
||||
ResizeResult result;
|
||||
result.regions.reserve(active_sprites.size());
|
||||
|
||||
constexpr int padding_px = 16;
|
||||
int content_w_mbs = sprite_w_mbs_ - 2;
|
||||
|
||||
for (int si = 0; si < (int)active_sprites.size(); si++) {
|
||||
slots_[si].sprite = std::move(active_sprites[si].sprite);
|
||||
slots_[si].current_frame = active_sprites[si].current_frame;
|
||||
slots_[si].active = true;
|
||||
|
||||
int col = si % new_cols;
|
||||
int row = si / new_cols;
|
||||
int color_x = col * new_stride_x * 16 + padding_px;
|
||||
int color_y = row * new_stride_y_val * 16 + padding_px;
|
||||
int alpha_x = color_x + (content_w_mbs + 1) * 16;
|
||||
|
||||
SpriteRegion region;
|
||||
region.slot = si;
|
||||
region.color = {color_x, color_y, content_w_, content_h_};
|
||||
region.alpha = {alpha_x, color_y, content_w_, content_h_};
|
||||
result.regions.push_back(region);
|
||||
}
|
||||
|
||||
plans_dirty_ = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MuxSurface::rebuild_row_plans_() {
|
||||
bool active_buf[4096];
|
||||
bool* active = (params_.max_slots <= 4096) ? active_buf
|
||||
: new bool[static_cast<size_t>(params_.max_slots)];
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
active[i] = slots_[i].active;
|
||||
}
|
||||
constexpr int padding_mbs = 1;
|
||||
mux::build_row_plans(active, params_.max_slots,
|
||||
sprite_w_mbs_, sprite_h_mbs_,
|
||||
padding_mbs,
|
||||
total_w_, total_h_,
|
||||
row_plans_, row_ops_);
|
||||
if (active != active_buf) delete[] active;
|
||||
}
|
||||
|
||||
void MuxSurface::advance_sprite(int slot) {
|
||||
if (slot < 0 || slot >= params_.max_slots) return;
|
||||
auto& sl = slots_[slot];
|
||||
if (!sl.active || !sl.sprite) return;
|
||||
|
||||
sl.needs_emit = true;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
std::expected<bool, Error> MuxSurface::emit_frame_if_needed(FrameSink sink) {
|
||||
if (!dirty_) return false;
|
||||
|
||||
if (plans_dirty_) {
|
||||
rebuild_row_plans_();
|
||||
plans_dirty_ = false;
|
||||
}
|
||||
|
||||
frame_num_++;
|
||||
|
||||
int max_slots = params_.max_slots;
|
||||
|
||||
for (int slot = 0; slot < max_slots; slot++) {
|
||||
auto& sl = slots_[slot];
|
||||
if (sl.active && sl.sprite && sl.needs_emit) {
|
||||
slot_infos_[slot].sprite = &(*sl.sprite);
|
||||
slot_infos_[slot].frame_index = sl.current_frame;
|
||||
} else {
|
||||
slot_infos_[slot].sprite = nullptr;
|
||||
slot_infos_[slot].frame_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int trailing_skip = mux::build_micro_ops(
|
||||
slot_infos_.data(),
|
||||
row_plans_.data(), total_h_,
|
||||
row_ops_.data(),
|
||||
sprite_w_mbs_, 1,
|
||||
micro_ops_);
|
||||
|
||||
std::span<uint8_t> out{buf_.get(), buf_size_};
|
||||
auto p_result = mux::write_p_frame_micro(
|
||||
micro_ops_.data(), static_cast<int>(micro_ops_.size()), trailing_skip,
|
||||
frame_num_,
|
||||
log2_max_frame_num_,
|
||||
params_.qp_delta_p,
|
||||
out);
|
||||
|
||||
if (!p_result) return std::unexpected(p_result.error());
|
||||
|
||||
sink(std::span<const uint8_t>{buf_.get(), *p_result});
|
||||
|
||||
// Advance emitted sprites and clear needs_emit
|
||||
for (int slot = 0; slot < max_slots; slot++) {
|
||||
auto& sl = slots_[slot];
|
||||
if (sl.needs_emit && sl.active && sl.sprite) {
|
||||
sl.current_frame++;
|
||||
if (sl.current_frame >= sl.sprite->num_frames)
|
||||
sl.current_frame = 0;
|
||||
}
|
||||
sl.needs_emit = false;
|
||||
}
|
||||
|
||||
dirty_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::expected<void, Error> MuxSurface::advance_frame(FrameSink sink) {
|
||||
// Mark all active sprites for emit, then emit + advance
|
||||
for (int i = 0; i < params_.max_slots; i++) {
|
||||
auto& sl = slots_[i];
|
||||
if (sl.active && sl.sprite)
|
||||
sl.needs_emit = true;
|
||||
}
|
||||
dirty_ = true;
|
||||
auto r = emit_frame_if_needed(std::move(sink));
|
||||
if (!r) return std::unexpected(r.error());
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace subcodec
|
||||
118
third-party/subcodec/src/mux_surface.h
vendored
Normal file
118
third-party/subcodec/src/mux_surface.h
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include "types.h"
|
||||
#include "error.h"
|
||||
#include "mbs_mux_common.h"
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
using FrameSink = std::function<void(std::span<const uint8_t>)>;
|
||||
|
||||
struct CompactionInfo {
|
||||
int active_sprites;
|
||||
int max_slots;
|
||||
int current_grid_mbs;
|
||||
int min_grid_mbs;
|
||||
};
|
||||
|
||||
class MuxSurface {
|
||||
public:
|
||||
struct Params {
|
||||
int sprite_width = 0; // Content width in pixels (multiple of 16)
|
||||
int sprite_height = 0; // Content height in pixels (multiple of 16)
|
||||
int max_slots = 0;
|
||||
uint8_t qp = 0;
|
||||
int8_t qp_delta_idr = 0;
|
||||
int8_t qp_delta_p = 0;
|
||||
};
|
||||
|
||||
struct SpriteRegion {
|
||||
int slot;
|
||||
struct Rect { int x, y, width, height; };
|
||||
Rect color;
|
||||
Rect alpha;
|
||||
};
|
||||
|
||||
struct ResizeResult {
|
||||
std::vector<SpriteRegion> regions;
|
||||
};
|
||||
|
||||
static std::expected<MuxSurface, Error>
|
||||
create(const Params& params, FrameSink sink);
|
||||
|
||||
~MuxSurface() = default;
|
||||
MuxSurface(MuxSurface&&) = default;
|
||||
MuxSurface& operator=(MuxSurface&&) = default;
|
||||
|
||||
std::expected<SpriteRegion, Error> add_sprite(const std::filesystem::path& mbs_path);
|
||||
std::expected<SpriteRegion, Error> add_sprite(MbsSprite sprite);
|
||||
void remove_sprite(int slot);
|
||||
std::expected<void, Error> advance_frame(FrameSink sink);
|
||||
void advance_sprite(int slot);
|
||||
std::expected<bool, Error> emit_frame_if_needed(FrameSink sink);
|
||||
|
||||
std::expected<ResizeResult, Error> resize(
|
||||
int new_max_slots,
|
||||
std::span<const uint8_t> decoded_y,
|
||||
std::span<const uint8_t> decoded_cb,
|
||||
std::span<const uint8_t> decoded_cr,
|
||||
int decoded_width,
|
||||
int decoded_height,
|
||||
int stride_y,
|
||||
int stride_cb,
|
||||
int stride_cr,
|
||||
FrameSink sink);
|
||||
|
||||
CompactionInfo check_compaction_opportunity() const;
|
||||
|
||||
[[nodiscard]] int width_mbs() const { return total_w_; }
|
||||
[[nodiscard]] int height_mbs() const { return total_h_; }
|
||||
[[nodiscard]] int frame_num() const { return frame_num_; }
|
||||
|
||||
private:
|
||||
MuxSurface() = default;
|
||||
|
||||
Params params_;
|
||||
int sprite_w_mbs_ = 0; // padded sprite width in MBs
|
||||
int sprite_h_mbs_ = 0; // padded sprite height in MBs
|
||||
int content_w_ = 0; // content width in pixels
|
||||
int content_h_ = 0; // content height in pixels
|
||||
int total_w_ = 0, total_h_ = 0;
|
||||
int cols_ = 0, rows_ = 0;
|
||||
int stride_x_ = 0, stride_y_ = 0;
|
||||
int frame_num_ = 0;
|
||||
int log2_max_frame_num_ = 8;
|
||||
int num_mbs_ = 0;
|
||||
|
||||
struct Slot {
|
||||
std::optional<MbsSprite> sprite;
|
||||
int current_frame = 0;
|
||||
bool active = false;
|
||||
bool needs_emit = false; // set by advance_sprite, cleared by emit
|
||||
};
|
||||
|
||||
void rebuild_row_plans_();
|
||||
|
||||
std::vector<Slot> slots_;
|
||||
std::vector<mux::SlotInfo> slot_infos_;
|
||||
std::unique_ptr<uint8_t[]> buf_;
|
||||
size_t buf_size_ = 0;
|
||||
std::vector<mux::CompositeRowPlan> row_plans_;
|
||||
std::vector<mux::RowOp> row_ops_;
|
||||
bool plans_dirty_ = true;
|
||||
bool dirty_ = false;
|
||||
std::unique_ptr<uint8_t[]> rbsp_buf_;
|
||||
size_t rbsp_buf_size_ = 0;
|
||||
std::vector<mux::MicroOp> micro_ops_;
|
||||
};
|
||||
|
||||
} // namespace subcodec
|
||||
166
third-party/subcodec/src/sprite_data.cpp
vendored
Normal file
166
third-party/subcodec/src/sprite_data.cpp
vendored
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#include "types.h"
|
||||
#include "mbs_format.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
void MbsSprite::set_frames(std::vector<MbsEncodedFrame>&& encoded) {
|
||||
num_frames = static_cast<uint16_t>(encoded.size());
|
||||
|
||||
size_t total_data = 0;
|
||||
size_t total_rows = 0;
|
||||
for (auto& ef : encoded) {
|
||||
total_data += ef.data.size();
|
||||
total_rows += ef.rows.size();
|
||||
}
|
||||
|
||||
bulk_data_ = std::make_unique_for_overwrite<uint8_t[]>(total_data);
|
||||
all_rows_.resize(total_rows);
|
||||
frames.resize(encoded.size());
|
||||
|
||||
size_t data_off = 0;
|
||||
size_t row_off = 0;
|
||||
for (size_t i = 0; i < encoded.size(); i++) {
|
||||
auto& ef = encoded[i];
|
||||
size_t dsz = ef.data.size();
|
||||
size_t rsz = ef.rows.size();
|
||||
|
||||
std::memcpy(bulk_data_.get() + data_off, ef.data.data(), dsz);
|
||||
|
||||
for (size_t r = 0; r < rsz; r++) {
|
||||
all_rows_[row_off + r] = ef.rows[r];
|
||||
if (ef.rows[r].blob_data) {
|
||||
ptrdiff_t blob_off = ef.rows[r].blob_data - ef.data.data();
|
||||
all_rows_[row_off + r].blob_data = bulk_data_.get() + data_off + blob_off;
|
||||
}
|
||||
}
|
||||
|
||||
frames[i].merged_rows = std::span<MbsRow>(all_rows_.data() + row_off, rsz);
|
||||
|
||||
data_off += dsz;
|
||||
row_off += rsz;
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<MbsSprite, Error> MbsSprite::load(const std::filesystem::path& path) {
|
||||
FILE* f = fopen(path.c_str(), "rb");
|
||||
if (!f) return std::unexpected(Error::IO_ERROR);
|
||||
|
||||
uint32_t magic;
|
||||
MbsSprite sp;
|
||||
uint8_t flags;
|
||||
|
||||
bool ok = true;
|
||||
ok &= fread(&magic, 4, 1, f) == 1;
|
||||
ok &= fread(&sp.width_mbs, 2, 1, f) == 1;
|
||||
ok &= fread(&sp.height_mbs, 2, 1, f) == 1;
|
||||
ok &= fread(&sp.num_frames, 2, 1, f) == 1;
|
||||
ok &= fread(&sp.qp, 1, 1, f) == 1;
|
||||
ok &= fread(&sp.qp_delta_idr, 1, 1, f) == 1;
|
||||
ok &= fread(&sp.qp_delta_p, 1, 1, f) == 1;
|
||||
ok &= fread(&flags, 1, 1, f) == 1;
|
||||
|
||||
if (!ok || magic != MBS_MAGIC_V6) {
|
||||
fclose(f);
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
|
||||
int h = sp.height_mbs;
|
||||
int nf = sp.num_frames;
|
||||
|
||||
long header_pos = ftell(f);
|
||||
fseek(f, 0, SEEK_END);
|
||||
long file_size = ftell(f);
|
||||
fseek(f, header_pos, SEEK_SET);
|
||||
size_t payload_size = static_cast<size_t>(file_size - header_pos);
|
||||
|
||||
sp.bulk_data_ = std::make_unique_for_overwrite<uint8_t[]>(payload_size);
|
||||
if (fread(sp.bulk_data_.get(), 1, payload_size, f) != payload_size) {
|
||||
fclose(f);
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
sp.all_rows_.resize(static_cast<size_t>(nf) * h);
|
||||
sp.frames.resize(nf);
|
||||
|
||||
const uint8_t* ptr = sp.bulk_data_.get();
|
||||
const uint8_t* end = ptr + payload_size;
|
||||
|
||||
for (int i = 0; i < nf; i++) {
|
||||
if (ptr + 4 > end) return std::unexpected(Error::PARSE_ERROR);
|
||||
uint32_t sz;
|
||||
std::memcpy(&sz, ptr, 4);
|
||||
ptr += 4;
|
||||
|
||||
if (ptr + sz > end) return std::unexpected(Error::PARSE_ERROR);
|
||||
const uint8_t* fp = ptr;
|
||||
const uint8_t* fe = ptr + sz;
|
||||
|
||||
size_t row_base = static_cast<size_t>(i) * h;
|
||||
for (int y = 0; y < h; y++) {
|
||||
if (fp + 6 > fe) return std::unexpected(Error::PARSE_ERROR);
|
||||
auto& row = sp.all_rows_[row_base + y];
|
||||
row.leading_skips = fp[0];
|
||||
row.trailing_skips = fp[1];
|
||||
row.blob_bit_count = static_cast<uint16_t>(fp[2] | (fp[3] << 8));
|
||||
row.leading_zero_bits = fp[4];
|
||||
row.trailing_zero_bits = fp[5];
|
||||
fp += 6;
|
||||
int blob_bytes = (row.bit_count() + 7) / 8;
|
||||
if (fp + blob_bytes > fe) return std::unexpected(Error::PARSE_ERROR);
|
||||
row.blob_data = (row.bit_count() > 0) ? fp : nullptr;
|
||||
fp += blob_bytes;
|
||||
}
|
||||
|
||||
sp.frames[i].merged_rows = std::span<MbsRow>(&sp.all_rows_[row_base], h);
|
||||
ptr += sz;
|
||||
}
|
||||
|
||||
return sp;
|
||||
}
|
||||
|
||||
std::expected<void, Error> MbsSprite::save(const std::filesystem::path& path) const {
|
||||
FILE* f = fopen(path.c_str(), "wb");
|
||||
if (!f) return std::unexpected(Error::IO_ERROR);
|
||||
|
||||
uint32_t magic = MBS_MAGIC_V6;
|
||||
uint8_t flags = 0;
|
||||
bool ok = true;
|
||||
|
||||
ok &= fwrite(&magic, 4, 1, f) == 1;
|
||||
ok &= fwrite(&width_mbs, 2, 1, f) == 1;
|
||||
ok &= fwrite(&height_mbs, 2, 1, f) == 1;
|
||||
ok &= fwrite(&num_frames, 2, 1, f) == 1;
|
||||
ok &= fwrite(&qp, 1, 1, f) == 1;
|
||||
ok &= fwrite(&qp_delta_idr, 1, 1, f) == 1;
|
||||
ok &= fwrite(&qp_delta_p, 1, 1, f) == 1;
|
||||
ok &= fwrite(&flags, 1, 1, f) == 1;
|
||||
|
||||
for (int i = 0; i < num_frames; i++) {
|
||||
uint32_t sz = 0;
|
||||
for (auto& row : frames[i].merged_rows) {
|
||||
sz += 6 + (row.bit_count() + 7) / 8;
|
||||
}
|
||||
ok &= fwrite(&sz, 4, 1, f) == 1;
|
||||
|
||||
for (auto& row : frames[i].merged_rows) {
|
||||
uint8_t hdr[6];
|
||||
hdr[0] = row.leading_skips;
|
||||
hdr[1] = row.trailing_skips;
|
||||
hdr[2] = static_cast<uint8_t>(row.blob_bit_count & 0xFF);
|
||||
hdr[3] = static_cast<uint8_t>((row.blob_bit_count >> 8) & 0xFF);
|
||||
hdr[4] = row.leading_zero_bits;
|
||||
hdr[5] = row.trailing_zero_bits;
|
||||
ok &= fwrite(hdr, 1, 6, f) == 6;
|
||||
int blob_bytes = (row.bit_count() + 7) / 8;
|
||||
if (blob_bytes > 0 && row.blob_data)
|
||||
ok &= fwrite(row.blob_data, 1, blob_bytes, f) == static_cast<size_t>(blob_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return ok ? std::expected<void, Error>{} : std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
|
||||
} // namespace subcodec
|
||||
291
third-party/subcodec/src/sprite_encode.cpp
vendored
Normal file
291
third-party/subcodec/src/sprite_encode.cpp
vendored
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
#include "sprite_encode.h"
|
||||
#include "h264_parser.h"
|
||||
|
||||
#include "codec_api.h"
|
||||
#include "codec_app_def.h"
|
||||
#include "codec_def.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
struct SpriteEncoder::Impl {
|
||||
ISVCEncoder* enc = nullptr;
|
||||
FrameParams parse_params{};
|
||||
int half_width = 0; // single sprite padded width
|
||||
int half_height = 0; // single sprite padded height
|
||||
int canvas_width = 0; // double-wide: half_width * 2
|
||||
int canvas_height = 0; // same as half_height
|
||||
int half_width_mbs = 0;
|
||||
int half_height_mbs = 0;
|
||||
int canvas_width_mbs = 0;
|
||||
H264Parser parser;
|
||||
|
||||
~Impl() {
|
||||
if (enc) {
|
||||
enc->Uninitialize();
|
||||
WelsDestroySVCEncoder(enc);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Find a slice NAL (type 1 or 5) in Annex B bitstream and parse it.
|
||||
static std::expected<std::vector<MacroblockData>, Error>
|
||||
find_and_parse_slice(H264Parser& parser,
|
||||
const uint8_t* data, size_t data_size,
|
||||
const FrameParams& params) {
|
||||
size_t pos = 0;
|
||||
while (pos + 4 < data_size) {
|
||||
int sc_len = 0;
|
||||
if (data[pos] == 0 && data[pos+1] == 0 && data[pos+2] == 0 && data[pos+3] == 1)
|
||||
sc_len = 4;
|
||||
else if (data[pos] == 0 && data[pos+1] == 0 && data[pos+2] == 1)
|
||||
sc_len = 3;
|
||||
|
||||
if (sc_len == 0) { pos++; continue; }
|
||||
|
||||
uint8_t nal_type = data[pos + sc_len] & 0x1F;
|
||||
|
||||
// Find end of this NAL
|
||||
size_t next_pos = pos + sc_len + 1;
|
||||
while (next_pos + 3 <= data_size) {
|
||||
if (data[next_pos] == 0 && data[next_pos+1] == 0 &&
|
||||
((next_pos + 2 < data_size && data[next_pos+2] == 1) ||
|
||||
(next_pos + 3 < data_size && data[next_pos+2] == 0 && data[next_pos+3] == 1)))
|
||||
break;
|
||||
next_pos++;
|
||||
}
|
||||
size_t nal_end = (next_pos + 3 <= data_size) ? next_pos : data_size;
|
||||
|
||||
if (nal_type == 1 || nal_type == 5) {
|
||||
const uint8_t* nal_data = data + pos;
|
||||
size_t nal_size = nal_end - pos;
|
||||
|
||||
// Ensure 4-byte start code for parser
|
||||
std::vector<uint8_t> normalized;
|
||||
if (sc_len == 3) {
|
||||
normalized.push_back(0x00);
|
||||
normalized.insert(normalized.end(), nal_data, nal_data + nal_size);
|
||||
nal_data = normalized.data();
|
||||
nal_size = normalized.size();
|
||||
}
|
||||
|
||||
return parser.parse_slice({nal_data, nal_size}, params);
|
||||
}
|
||||
|
||||
pos = nal_end;
|
||||
}
|
||||
return std::unexpected(Error::PARSE_ERROR);
|
||||
}
|
||||
|
||||
SpriteEncoder::SpriteEncoder() = default;
|
||||
SpriteEncoder::~SpriteEncoder() = default;
|
||||
SpriteEncoder::SpriteEncoder(SpriteEncoder&&) noexcept = default;
|
||||
SpriteEncoder& SpriteEncoder::operator=(SpriteEncoder&&) noexcept = default;
|
||||
|
||||
std::expected<SpriteEncoder, Error> SpriteEncoder::create(const Params& params) {
|
||||
if (params.width <= 0 || params.height <= 0 ||
|
||||
params.width % 16 != 0 || params.height % 16 != 0)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
constexpr int padding_px = 16;
|
||||
int padded_width = params.width + 2 * padding_px;
|
||||
int padded_height = params.height + 2 * padding_px;
|
||||
int canvas_width = padded_width * 2;
|
||||
int canvas_height = padded_height;
|
||||
|
||||
ISVCEncoder* enc = nullptr;
|
||||
if (WelsCreateSVCEncoder(&enc) != 0 || !enc)
|
||||
return std::unexpected(Error::ENCODE_ERROR);
|
||||
|
||||
SEncParamExt eparam;
|
||||
enc->GetDefaultParams(&eparam);
|
||||
eparam.iUsageType = CAMERA_VIDEO_REAL_TIME;
|
||||
eparam.iPicWidth = canvas_width;
|
||||
eparam.iPicHeight = canvas_height;
|
||||
eparam.fMaxFrameRate = 30.0f;
|
||||
eparam.iRCMode = RC_OFF_MODE;
|
||||
eparam.iEntropyCodingModeFlag = 0; // CAVLC
|
||||
eparam.iSpatialLayerNum = 1;
|
||||
eparam.iTemporalLayerNum = 1;
|
||||
eparam.bEnableFrameSkip = false;
|
||||
eparam.iMultipleThreadIdc = 1;
|
||||
eparam.sSpatialLayers[0].uiProfileIdc = PRO_BASELINE;
|
||||
eparam.sSpatialLayers[0].iVideoWidth = canvas_width;
|
||||
eparam.sSpatialLayers[0].iVideoHeight = canvas_height;
|
||||
eparam.sSpatialLayers[0].fFrameRate = 30.0f;
|
||||
eparam.sSpatialLayers[0].iSpatialBitrate = 500000;
|
||||
eparam.sSpatialLayers[0].iMaxSpatialBitrate = 500000;
|
||||
eparam.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SINGLE_SLICE;
|
||||
eparam.sSpatialLayers[0].iDLayerQp = params.qp;
|
||||
eparam.uiIntraPeriod = 0; // Only first frame is IDR
|
||||
eparam.iNumRefFrame = 1;
|
||||
eparam.iLoopFilterDisableIdc = 1;
|
||||
eparam.bSubcodecMode = true; // Enable subcodec sprite-compositing constraints
|
||||
eparam.bEnableAdaptiveQuant = false;
|
||||
eparam.iMinQp = params.qp;
|
||||
eparam.iMaxQp = params.qp;
|
||||
|
||||
if (enc->InitializeExt(&eparam) != 0) {
|
||||
WelsDestroySVCEncoder(enc);
|
||||
return std::unexpected(Error::ENCODE_ERROR);
|
||||
}
|
||||
|
||||
int videoFormat = videoFormatI420;
|
||||
enc->SetOption(ENCODER_OPTION_DATAFORMAT, &videoFormat);
|
||||
|
||||
auto impl = std::make_unique<Impl>();
|
||||
impl->enc = enc;
|
||||
impl->half_width = padded_width;
|
||||
impl->half_height = padded_height;
|
||||
impl->canvas_width = canvas_width;
|
||||
impl->canvas_height = canvas_height;
|
||||
impl->half_width_mbs = padded_width / 16;
|
||||
impl->half_height_mbs = padded_height / 16;
|
||||
impl->canvas_width_mbs = canvas_width / 16;
|
||||
|
||||
// Parse params use canvas dimensions (what OpenH264 actually encoded)
|
||||
impl->parse_params.width_mbs = static_cast<uint16_t>(impl->canvas_width_mbs);
|
||||
impl->parse_params.height_mbs = static_cast<uint16_t>(impl->half_height_mbs);
|
||||
impl->parse_params.log2_max_frame_num = 4;
|
||||
impl->parse_params.pic_order_cnt_type = 0;
|
||||
impl->parse_params.log2_max_pic_order_cnt_lsb = 5;
|
||||
impl->parse_params.qp = static_cast<uint8_t>(params.qp);
|
||||
|
||||
SpriteEncoder se;
|
||||
se.impl_ = std::move(impl);
|
||||
return se;
|
||||
}
|
||||
|
||||
std::expected<EncodeResult, Error> SpriteEncoder::encode(
|
||||
const uint8_t* y, int y_stride,
|
||||
const uint8_t* cb, int cb_stride,
|
||||
const uint8_t* cr, int cr_stride,
|
||||
const uint8_t* alpha, int alpha_stride,
|
||||
int frame_index,
|
||||
std::vector<uint8_t>* out_nal_data) {
|
||||
|
||||
if (!impl_ || !y || !cb || !cr || !alpha)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
int hw = impl_->half_width;
|
||||
int hh = impl_->half_height;
|
||||
int cw = impl_->canvas_width;
|
||||
int ch = impl_->canvas_height;
|
||||
int half_chroma_w = hw / 2;
|
||||
int half_chroma_h = hh / 2;
|
||||
int canvas_chroma_w = cw / 2;
|
||||
int canvas_chroma_h = ch / 2;
|
||||
|
||||
// Build double-wide YUV canvas
|
||||
// Left half: color Y/Cb/Cr; Right half: alpha as luma, Cb=Cr=128
|
||||
std::vector<uint8_t> canvas_y(cw * ch, 0);
|
||||
std::vector<uint8_t> canvas_cb(canvas_chroma_w * canvas_chroma_h, 128);
|
||||
std::vector<uint8_t> canvas_cr(canvas_chroma_w * canvas_chroma_h, 128);
|
||||
|
||||
// Copy color luma to left half
|
||||
for (int row = 0; row < hh; row++)
|
||||
memcpy(canvas_y.data() + row * cw, y + row * y_stride, hw);
|
||||
|
||||
// Copy alpha as luma to right half
|
||||
for (int row = 0; row < hh; row++)
|
||||
memcpy(canvas_y.data() + row * cw + hw, alpha + row * alpha_stride, hw);
|
||||
|
||||
// Copy color chroma to left half (right half stays at 128)
|
||||
for (int row = 0; row < half_chroma_h; row++) {
|
||||
memcpy(canvas_cb.data() + row * canvas_chroma_w, cb + row * cb_stride, half_chroma_w);
|
||||
memcpy(canvas_cr.data() + row * canvas_chroma_w, cr + row * cr_stride, half_chroma_w);
|
||||
}
|
||||
|
||||
SSourcePicture pic;
|
||||
memset(&pic, 0, sizeof(pic));
|
||||
pic.iColorFormat = videoFormatI420;
|
||||
pic.iPicWidth = cw;
|
||||
pic.iPicHeight = ch;
|
||||
pic.iStride[0] = cw;
|
||||
pic.iStride[1] = canvas_chroma_w;
|
||||
pic.iStride[2] = canvas_chroma_w;
|
||||
pic.pData[0] = canvas_y.data();
|
||||
pic.pData[1] = canvas_cb.data();
|
||||
pic.pData[2] = canvas_cr.data();
|
||||
pic.uiTimeStamp = frame_index * 33;
|
||||
|
||||
SFrameBSInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
int rv = impl_->enc->EncodeFrame(&pic, &info);
|
||||
if (rv != cmResultSuccess) {
|
||||
fprintf(stderr, "SpriteEncoder: EncodeFrame failed frame %d (rv=%d)\n", frame_index, rv);
|
||||
return std::unexpected(Error::ENCODE_ERROR);
|
||||
}
|
||||
|
||||
if (info.eFrameType == videoFrameTypeSkip) {
|
||||
fprintf(stderr, "SpriteEncoder: unexpected skip frame %d\n", frame_index);
|
||||
return std::unexpected(Error::ENCODE_ERROR);
|
||||
}
|
||||
|
||||
// Collect all NAL data
|
||||
std::vector<uint8_t> frame_nals;
|
||||
for (int layer = 0; layer < info.iLayerNum; layer++) {
|
||||
SLayerBSInfo* layerInfo = &info.sLayerInfo[layer];
|
||||
uint8_t* buf = layerInfo->pBsBuf;
|
||||
for (int nal = 0; nal < layerInfo->iNalCount; nal++) {
|
||||
int nalLen = layerInfo->pNalLengthInByte[nal];
|
||||
frame_nals.insert(frame_nals.end(), buf, buf + nalLen);
|
||||
buf += nalLen;
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally return NAL data copy
|
||||
if (out_nal_data) {
|
||||
*out_nal_data = frame_nals;
|
||||
}
|
||||
|
||||
// Parse full double-wide slice into macroblock data
|
||||
auto parse_result = find_and_parse_slice(
|
||||
impl_->parser, frame_nals.data(), frame_nals.size(), impl_->parse_params);
|
||||
if (!parse_result) {
|
||||
fprintf(stderr, "SpriteEncoder: parse failed frame %d\n", frame_index);
|
||||
return std::unexpected(parse_result.error());
|
||||
}
|
||||
|
||||
auto& all_mbs = *parse_result;
|
||||
int hwm = impl_->half_width_mbs;
|
||||
int hhm = impl_->half_height_mbs;
|
||||
int cwm = impl_->canvas_width_mbs;
|
||||
|
||||
// Split into color (left half) and alpha (right half)
|
||||
std::vector<MacroblockData> color(hwm * hhm);
|
||||
std::vector<MacroblockData> alpha_mbs(hwm * hhm);
|
||||
|
||||
for (int mb_y = 0; mb_y < hhm; mb_y++) {
|
||||
for (int mb_x = 0; mb_x < hwm; mb_x++) {
|
||||
color[mb_y * hwm + mb_x] = all_mbs[mb_y * cwm + mb_x];
|
||||
alpha_mbs[mb_y * hwm + mb_x] = all_mbs[mb_y * cwm + hwm + mb_x];
|
||||
}
|
||||
}
|
||||
|
||||
// For IDR frames, mark padding MBs as SKIP in both halves
|
||||
if (frame_index == 0) {
|
||||
int padding = 1; // matches iPaddingMbs set in create
|
||||
for (int mb_y = 0; mb_y < hhm; mb_y++) {
|
||||
for (int mb_x = 0; mb_x < hwm; mb_x++) {
|
||||
if (mb_x < padding || mb_x >= hwm - padding ||
|
||||
mb_y < padding || mb_y >= hhm - padding) {
|
||||
MacroblockData& cmb = color[mb_y * hwm + mb_x];
|
||||
cmb = MacroblockData{};
|
||||
cmb.mb_type = MbType::SKIP;
|
||||
MacroblockData& amb = alpha_mbs[mb_y * hwm + mb_x];
|
||||
amb = MacroblockData{};
|
||||
amb.mb_type = MbType::SKIP;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EncodeResult{std::move(color), std::move(alpha_mbs)};
|
||||
}
|
||||
|
||||
} // namespace subcodec
|
||||
46
third-party/subcodec/src/sprite_encode.h
vendored
Normal file
46
third-party/subcodec/src/sprite_encode.h
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <expected>
|
||||
#include "types.h"
|
||||
#include "error.h"
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
struct EncodeResult {
|
||||
std::vector<MacroblockData> color;
|
||||
std::vector<MacroblockData> alpha;
|
||||
};
|
||||
|
||||
class SpriteEncoder {
|
||||
public:
|
||||
struct Params {
|
||||
int width = 0; // Content width in pixels (multiple of 16)
|
||||
int height = 0; // Content height in pixels (multiple of 16)
|
||||
int qp = 26;
|
||||
};
|
||||
|
||||
static std::expected<SpriteEncoder, Error> create(const Params& params);
|
||||
~SpriteEncoder();
|
||||
|
||||
SpriteEncoder(SpriteEncoder&&) noexcept;
|
||||
SpriteEncoder& operator=(SpriteEncoder&&) noexcept;
|
||||
|
||||
std::expected<EncodeResult, Error> encode(
|
||||
const uint8_t* y, int y_stride,
|
||||
const uint8_t* cb, int cb_stride,
|
||||
const uint8_t* cr, int cr_stride,
|
||||
const uint8_t* alpha, int alpha_stride,
|
||||
int frame_index,
|
||||
std::vector<uint8_t>* out_nal_data = nullptr);
|
||||
|
||||
private:
|
||||
SpriteEncoder();
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace subcodec
|
||||
219
third-party/subcodec/src/sprite_extractor.cpp
vendored
Normal file
219
third-party/subcodec/src/sprite_extractor.cpp
vendored
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
#include "sprite_extractor.h"
|
||||
#include "sprite_encode.h"
|
||||
#include "mbs_encode.h"
|
||||
#include "mbs_format.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
|
||||
struct SpriteExtractor::Impl {
|
||||
SpriteEncoder encoder;
|
||||
FILE* file = nullptr;
|
||||
std::filesystem::path output_path;
|
||||
FrameParams frame_params{};
|
||||
uint16_t frame_count = 0;
|
||||
int sprite_size = 0;
|
||||
int padded_size = 0;
|
||||
int padded_stride = 0;
|
||||
// Reusable padded YUV buffers
|
||||
std::vector<uint8_t> pad_y;
|
||||
std::vector<uint8_t> pad_cb;
|
||||
std::vector<uint8_t> pad_cr;
|
||||
std::vector<uint8_t> pad_alpha;
|
||||
bool failed = false;
|
||||
|
||||
Impl(SpriteEncoder&& enc) : encoder(std::move(enc)) {}
|
||||
|
||||
~Impl() {
|
||||
if (file) {
|
||||
fclose(file);
|
||||
// Remove partial file if finalize() was never called
|
||||
std::filesystem::remove(output_path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SpriteExtractor::SpriteExtractor() = default;
|
||||
SpriteExtractor::~SpriteExtractor() = default;
|
||||
SpriteExtractor::SpriteExtractor(SpriteExtractor&&) noexcept = default;
|
||||
SpriteExtractor& SpriteExtractor::operator=(SpriteExtractor&&) noexcept = default;
|
||||
|
||||
std::expected<SpriteExtractor, Error> SpriteExtractor::create(
|
||||
const Params& params, const std::filesystem::path& output_path) {
|
||||
|
||||
if (params.sprite_size <= 0 || params.sprite_size % 16 != 0)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
constexpr int padding_px = 16;
|
||||
int padded_size = params.sprite_size + 2 * padding_px;
|
||||
|
||||
auto enc_result = SpriteEncoder::create({params.sprite_size, params.sprite_size, params.qp});
|
||||
if (!enc_result)
|
||||
return std::unexpected(enc_result.error());
|
||||
|
||||
FILE* f = fopen(output_path.c_str(), "wb");
|
||||
if (!f)
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
|
||||
auto impl = std::make_unique<Impl>(std::move(*enc_result));
|
||||
impl->file = f;
|
||||
impl->output_path = output_path;
|
||||
impl->sprite_size = params.sprite_size;
|
||||
impl->padded_size = padded_size;
|
||||
|
||||
uint16_t width_mbs = static_cast<uint16_t>(padded_size / 16);
|
||||
uint16_t height_mbs = static_cast<uint16_t>(padded_size / 16);
|
||||
|
||||
impl->frame_params.width_mbs = width_mbs;
|
||||
impl->frame_params.height_mbs = height_mbs;
|
||||
impl->frame_params.qp = static_cast<uint8_t>(params.qp);
|
||||
|
||||
// Allocate padded YUV buffers (reused across frames)
|
||||
impl->padded_stride = padded_size;
|
||||
int chroma_size = (padded_size / 2) * (padded_size / 2);
|
||||
impl->pad_y.resize(padded_size * padded_size);
|
||||
impl->pad_cb.resize(chroma_size);
|
||||
impl->pad_cr.resize(chroma_size);
|
||||
impl->pad_alpha.resize(padded_size * padded_size, 0); // black = transparent
|
||||
|
||||
// Write MBS v6 header with num_frames=0 (patched in finalize)
|
||||
uint32_t magic = MBS_MAGIC_V6;
|
||||
uint16_t num_frames = 0;
|
||||
uint8_t qp = static_cast<uint8_t>(params.qp);
|
||||
uint8_t zero = 0;
|
||||
uint8_t flags = 0; // reserved
|
||||
bool ok = true;
|
||||
ok &= fwrite(&magic, 4, 1, f) == 1;
|
||||
ok &= fwrite(&width_mbs, 2, 1, f) == 1;
|
||||
ok &= fwrite(&height_mbs, 2, 1, f) == 1;
|
||||
ok &= fwrite(&num_frames, 2, 1, f) == 1;
|
||||
ok &= fwrite(&qp, 1, 1, f) == 1;
|
||||
ok &= fwrite(&zero, 1, 1, f) == 1; // qp_delta_idr
|
||||
ok &= fwrite(&zero, 1, 1, f) == 1; // qp_delta_p
|
||||
ok &= fwrite(&flags, 1, 1, f) == 1; // flags (reserved)
|
||||
|
||||
if (!ok) {
|
||||
fclose(f);
|
||||
impl->file = nullptr;
|
||||
std::filesystem::remove(output_path);
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
|
||||
SpriteExtractor ext;
|
||||
ext.impl_ = std::move(impl);
|
||||
return ext;
|
||||
}
|
||||
|
||||
std::expected<void, Error> SpriteExtractor::add_frame(
|
||||
const uint8_t* y, int y_stride,
|
||||
const uint8_t* cb, int cb_stride,
|
||||
const uint8_t* cr, int cr_stride,
|
||||
const uint8_t* alpha, int alpha_stride) {
|
||||
|
||||
if (!impl_ || !impl_->file || impl_->failed)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
int ss = impl_->sprite_size;
|
||||
constexpr int pp = 16;
|
||||
int ps = impl_->padded_size;
|
||||
int chroma_pp = pp / 2;
|
||||
int chroma_ss = ss / 2;
|
||||
int chroma_ps = ps / 2;
|
||||
|
||||
// Clear padded buffers to black (Y=0, Cb=128, Cr=128)
|
||||
memset(impl_->pad_y.data(), 0, impl_->pad_y.size());
|
||||
memset(impl_->pad_cb.data(), 128, impl_->pad_cb.size());
|
||||
memset(impl_->pad_cr.data(), 128, impl_->pad_cr.size());
|
||||
|
||||
// Copy luma
|
||||
for (int row = 0; row < ss; row++) {
|
||||
memcpy(impl_->pad_y.data() + (row + pp) * ps + pp,
|
||||
y + row * y_stride, ss);
|
||||
}
|
||||
|
||||
// Copy chroma
|
||||
for (int row = 0; row < chroma_ss; row++) {
|
||||
memcpy(impl_->pad_cb.data() + (row + chroma_pp) * chroma_ps + chroma_pp,
|
||||
cb + row * cb_stride, chroma_ss);
|
||||
memcpy(impl_->pad_cr.data() + (row + chroma_pp) * chroma_ps + chroma_pp,
|
||||
cr + row * cr_stride, chroma_ss);
|
||||
}
|
||||
|
||||
// Clear alpha padding to 0 (transparent) then copy alpha content
|
||||
memset(impl_->pad_alpha.data(), 0, impl_->pad_alpha.size());
|
||||
for (int row = 0; row < ss; row++) {
|
||||
memcpy(impl_->pad_alpha.data() + (row + pp) * ps + pp,
|
||||
alpha + row * alpha_stride, ss);
|
||||
}
|
||||
|
||||
// Encode with OpenH264 + parse into MacroblockData
|
||||
auto encode_result = impl_->encoder.encode(
|
||||
impl_->pad_y.data(), ps,
|
||||
impl_->pad_cb.data(), chroma_ps,
|
||||
impl_->pad_cr.data(), chroma_ps,
|
||||
impl_->pad_alpha.data(), ps,
|
||||
impl_->frame_count, nullptr);
|
||||
|
||||
if (!encode_result) {
|
||||
impl_->failed = true;
|
||||
return std::unexpected(encode_result.error());
|
||||
}
|
||||
|
||||
// MBS-encode color+alpha as merged frame
|
||||
auto merged_mbs = mbs::encode_frame_merged(
|
||||
impl_->frame_params, encode_result->color.data(),
|
||||
impl_->frame_params, encode_result->alpha.data(),
|
||||
impl_->frame_params.width_mbs, 1 /* padding */);
|
||||
if (merged_mbs.data.empty()) {
|
||||
impl_->failed = true;
|
||||
return std::unexpected(Error::ENCODE_ERROR);
|
||||
}
|
||||
|
||||
// Write [frame_data_size][merged_row_data] to file
|
||||
uint32_t sz = static_cast<uint32_t>(merged_mbs.data.size());
|
||||
bool ok = true;
|
||||
ok &= fwrite(&sz, 4, 1, impl_->file) == 1;
|
||||
ok &= fwrite(merged_mbs.data.data(), 1, merged_mbs.data.size(), impl_->file)
|
||||
== merged_mbs.data.size();
|
||||
|
||||
if (!ok) {
|
||||
impl_->failed = true;
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
|
||||
impl_->frame_count++;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, Error> SpriteExtractor::finalize() {
|
||||
if (!impl_ || !impl_->file)
|
||||
return std::unexpected(Error::INVALID_INPUT);
|
||||
|
||||
if (impl_->failed) {
|
||||
fclose(impl_->file);
|
||||
impl_->file = nullptr;
|
||||
std::filesystem::remove(impl_->output_path);
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
|
||||
// Patch num_frames in header (offset 8: magic(4) + width(2) + height(2))
|
||||
fseek(impl_->file, 8, SEEK_SET);
|
||||
uint16_t nf = impl_->frame_count;
|
||||
bool ok = fwrite(&nf, 2, 1, impl_->file) == 1;
|
||||
|
||||
fclose(impl_->file);
|
||||
impl_->file = nullptr;
|
||||
|
||||
if (!ok) {
|
||||
std::filesystem::remove(impl_->output_path);
|
||||
return std::unexpected(Error::IO_ERROR);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace subcodec
|
||||
39
third-party/subcodec/src/sprite_extractor.h
vendored
Normal file
39
third-party/subcodec/src/sprite_extractor.h
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include "error.h"
|
||||
|
||||
namespace subcodec {
|
||||
|
||||
class SpriteExtractor {
|
||||
public:
|
||||
struct Params {
|
||||
int sprite_size; // content size in pixels (must be multiple of 16)
|
||||
int qp = 26; // quantization parameter
|
||||
};
|
||||
|
||||
static std::expected<SpriteExtractor, Error> create(
|
||||
const Params& params, const std::filesystem::path& output_path);
|
||||
|
||||
std::expected<void, Error> add_frame(
|
||||
const uint8_t* y, int y_stride,
|
||||
const uint8_t* cb, int cb_stride,
|
||||
const uint8_t* cr, int cr_stride,
|
||||
const uint8_t* alpha, int alpha_stride);
|
||||
|
||||
std::expected<void, Error> finalize();
|
||||
|
||||
~SpriteExtractor();
|
||||
SpriteExtractor(SpriteExtractor&&) noexcept;
|
||||
SpriteExtractor& operator=(SpriteExtractor&&) noexcept;
|
||||
|
||||
private:
|
||||
SpriteExtractor();
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace subcodec
|
||||
44
third-party/subcodec/src/tables.h
vendored
Normal file
44
third-party/subcodec/src/tables.h
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace subcodec::tables {
|
||||
|
||||
// Table 9-4(b): CBP to exp-golomb codeNum for Inter prediction
|
||||
// Indexed by [cbp_chroma * 16 + cbp_luma]
|
||||
inline constexpr uint8_t cbp_to_code_inter[48] = {
|
||||
// cbp_chroma = 0
|
||||
0, 2, 3, 7, 4, 8, 17, 13, 5, 18, 9, 14, 10, 15, 16, 11,
|
||||
// cbp_chroma = 1
|
||||
1, 32, 33, 36, 34, 37, 44, 40, 35, 45, 38, 41, 39, 42, 43, 19,
|
||||
// cbp_chroma = 2
|
||||
6, 24, 25, 20, 26, 21, 46, 28, 27, 47, 22, 29, 23, 30, 31, 12,
|
||||
};
|
||||
|
||||
// Table 9-4(a): CBP to exp-golomb codeNum for Intra prediction
|
||||
// Indexed by [cbp_chroma * 16 + cbp_luma]
|
||||
inline constexpr uint8_t cbp_to_code_intra[48] = {
|
||||
// cbp_chroma = 0
|
||||
3, 29, 30, 17, 31, 18, 37, 8, 32, 38, 19, 9, 20, 10, 11, 2,
|
||||
// cbp_chroma = 1
|
||||
16, 33, 34, 21, 35, 22, 39, 4, 36, 40, 23, 5, 24, 6, 7, 1,
|
||||
// cbp_chroma = 2
|
||||
41, 42, 43, 25, 44, 26, 46, 12, 45, 47, 27, 13, 28, 14, 15, 0,
|
||||
};
|
||||
|
||||
// H.264 Table 6-9: 4x4 block scan order within a macroblock
|
||||
// 8x8 block N contains 4x4 blocks N*4..N*4+3
|
||||
inline constexpr int luma_block_order[16] = {
|
||||
0, 1, 2, 3, // 8x8 block 0
|
||||
4, 5, 6, 7, // 8x8 block 1
|
||||
8, 9, 10, 11, // 8x8 block 2
|
||||
12, 13, 14, 15 // 8x8 block 3
|
||||
};
|
||||
|
||||
// Map 4x4 block index to its 8x8 parent block
|
||||
inline constexpr int block_to_8x8[16] = {
|
||||
0, 0, 0, 0, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 3, 3, 3, 3
|
||||
};
|
||||
|
||||
} // namespace subcodec::tables
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue